@twango/twiki 0.0.0-stage → 0.1.1
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/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { resolveProject } from './lib/project-config';
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { publicAssetManifest } from './lib/public-assets';
|
|
6
|
+
import { buildMode, runEditionBuilds } from './lib/edition-builds';
|
|
7
|
+
|
|
8
|
+
const site = path.resolve(import.meta.dir, '..');
|
|
9
|
+
const options = process.argv.slice(2);
|
|
10
|
+
if (
|
|
11
|
+
options.length > 1 ||
|
|
12
|
+
options.some((value) => !['--app', '--content', '--static'].includes(value))
|
|
13
|
+
)
|
|
14
|
+
throw new Error('Use --app, --content, --static, or no option for the complete SSR build');
|
|
15
|
+
if (options[0] === '--static') {
|
|
16
|
+
process.env.VITE_STATIC_EXPORT = 'true';
|
|
17
|
+
delete process.env.WISCONSIN_APP_STATIC_DIR;
|
|
18
|
+
delete process.env.WISCONSIN_SKIP_CONTENT;
|
|
19
|
+
const mode = buildMode();
|
|
20
|
+
if (mode !== 'serial') await isolatedBuild(mode);
|
|
21
|
+
else serialBuild();
|
|
22
|
+
} else await runtimeBuild(options[0] === '--app');
|
|
23
|
+
|
|
24
|
+
async function command(
|
|
25
|
+
binary: string,
|
|
26
|
+
args: string[],
|
|
27
|
+
environment: NodeJS.ProcessEnv,
|
|
28
|
+
signal: AbortSignal
|
|
29
|
+
) {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
const child = spawn(binary, args, {
|
|
32
|
+
cwd: site,
|
|
33
|
+
env: environment,
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
detached: process.platform !== 'win32'
|
|
36
|
+
});
|
|
37
|
+
let stopping: Promise<void> | undefined;
|
|
38
|
+
const stop = () => {
|
|
39
|
+
if (stopping || !child.pid) return;
|
|
40
|
+
const send = (kind: NodeJS.Signals) => {
|
|
41
|
+
try {
|
|
42
|
+
if (process.platform === 'win32') child.kill(kind);
|
|
43
|
+
else process.kill(-child.pid!, kind);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
send('SIGTERM');
|
|
49
|
+
stopping = new Promise((resolve) =>
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
send('SIGKILL');
|
|
52
|
+
resolve();
|
|
53
|
+
}, 2000)
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
57
|
+
try {
|
|
58
|
+
await new Promise<void>((resolve, reject) => {
|
|
59
|
+
child.once('error', reject);
|
|
60
|
+
child.once('close', (code, killed) =>
|
|
61
|
+
code === 0 ? resolve() : reject(new Error(`Build command failed (${code ?? killed})`))
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
await stopping;
|
|
66
|
+
signal.removeEventListener('abort', stop);
|
|
67
|
+
}
|
|
68
|
+
signal.throwIfAborted();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runtimeBuild(appOnly: boolean) {
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const abort = new AbortController();
|
|
74
|
+
const interrupt = () => abort.abort(new Error('Build interrupted'));
|
|
75
|
+
process.on('SIGINT', interrupt);
|
|
76
|
+
process.on('SIGTERM', interrupt);
|
|
77
|
+
let staging: string | undefined;
|
|
78
|
+
try {
|
|
79
|
+
const app = await import('./lib/application-cache');
|
|
80
|
+
const { captureAssetInventory, assetContentType, validateAssetInventory } =
|
|
81
|
+
await import('./lib/deployment-manifest');
|
|
82
|
+
const { captureAssetHashes, flushAssetHashCache } = await import('./lib/asset-hash-cache');
|
|
83
|
+
const { prepareWorker, loadPreparedWorker, buildDeploymentSnapshot } =
|
|
84
|
+
await import('./lib/deployment-prepare');
|
|
85
|
+
const { readDeploymentConfig } = await import('./lib/deployment-config');
|
|
86
|
+
const packaging = await import('./lib/runtime-packaging');
|
|
87
|
+
const { contentInputFingerprint } = await import('./lib/dev-state');
|
|
88
|
+
const environment = app.applicationEnvironment();
|
|
89
|
+
const identity = app.applicationCacheIdentity(site, environment);
|
|
90
|
+
const contentRepo = resolveProject(site).root;
|
|
91
|
+
const contentInputs = appOnly ? '' : contentInputFingerprint(contentRepo, identity.identity);
|
|
92
|
+
let cached = app.loadApplicationCache(site, identity);
|
|
93
|
+
if (cached)
|
|
94
|
+
try {
|
|
95
|
+
validateAssetInventory(
|
|
96
|
+
JSON.parse(
|
|
97
|
+
readFileSync(path.join(cached.directory, 'kit/application-assets.json'), 'utf8')
|
|
98
|
+
),
|
|
99
|
+
identity.identity
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
cached = undefined;
|
|
103
|
+
}
|
|
104
|
+
const appStarted = performance.now();
|
|
105
|
+
if (!cached) {
|
|
106
|
+
mkdirSync(path.join(site, 'build'), { recursive: true });
|
|
107
|
+
const temporary = mkdtempSync(path.join(site, 'build/.application-build-'));
|
|
108
|
+
const previous = path.join(temporary, 'previous');
|
|
109
|
+
const output = path.join(site, 'build/.svelte-kit');
|
|
110
|
+
let moved = false,
|
|
111
|
+
preserve = false;
|
|
112
|
+
try {
|
|
113
|
+
app.prepareApplicationStatic(site, path.join(temporary, 'static'));
|
|
114
|
+
try {
|
|
115
|
+
renameSync(output, previous);
|
|
116
|
+
moved = true;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
119
|
+
}
|
|
120
|
+
await command(
|
|
121
|
+
'bun',
|
|
122
|
+
['x', 'vite', 'build'],
|
|
123
|
+
{
|
|
124
|
+
...environment,
|
|
125
|
+
WISCONSIN_APP_STATIC_DIR: path.join(temporary, 'static'),
|
|
126
|
+
WISCONSIN_APPLICATION_VERSION: identity.applicationVersion
|
|
127
|
+
},
|
|
128
|
+
abort.signal
|
|
129
|
+
);
|
|
130
|
+
await prepareWorker(site, path.join(output, 'prepared-worker'));
|
|
131
|
+
const inventory = await captureAssetInventory(
|
|
132
|
+
path.join(output, 'cloudflare'),
|
|
133
|
+
identity.identity,
|
|
134
|
+
{
|
|
135
|
+
resolveAsset: async (absolute, filename) => ({
|
|
136
|
+
...captureAssetHashes(site, absolute),
|
|
137
|
+
contentType: assetContentType(filename)
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
writeFileSync(path.join(output, 'application-assets.json'), JSON.stringify(inventory));
|
|
142
|
+
if (app.applicationCacheIdentity(site, environment).identity !== identity.identity)
|
|
143
|
+
throw new Error('Application inputs changed during compilation');
|
|
144
|
+
cached = app.saveApplicationCache(site, identity);
|
|
145
|
+
} finally {
|
|
146
|
+
rmSync(output, { recursive: true, force: true });
|
|
147
|
+
if (moved)
|
|
148
|
+
try {
|
|
149
|
+
renameSync(previous, output);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
preserve = true;
|
|
152
|
+
throw new Error('Previous application output preserved for recovery', { cause: error });
|
|
153
|
+
}
|
|
154
|
+
if (!preserve) rmSync(temporary, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
console.log(
|
|
157
|
+
`build: application compiled in ${((performance.now() - appStarted) / 1000).toFixed(2)}s`
|
|
158
|
+
);
|
|
159
|
+
} else
|
|
160
|
+
console.log(
|
|
161
|
+
`build: application reused in ${((performance.now() - appStarted) / 1000).toFixed(2)}s`
|
|
162
|
+
);
|
|
163
|
+
abort.signal.throwIfAborted();
|
|
164
|
+
if (appOnly) {
|
|
165
|
+
flushAssetHashCache(site);
|
|
166
|
+
console.log(`build: app completed in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
mkdirSync(path.join(site, 'build'), { recursive: true });
|
|
170
|
+
staging = mkdtempSync(path.join(site, 'build/.runtime-build-'));
|
|
171
|
+
const product = path.join(staging, 'kit');
|
|
172
|
+
app.stageApplication(cached!, product);
|
|
173
|
+
const appInventory = validateAssetInventory(
|
|
174
|
+
JSON.parse(readFileSync(path.join(product, 'application-assets.json'), 'utf8')),
|
|
175
|
+
identity.identity
|
|
176
|
+
);
|
|
177
|
+
const editions: import('./lib/runtime-packaging').CapturedEdition[] = [];
|
|
178
|
+
for (const edition of ['public', 'full'] as const) {
|
|
179
|
+
const editionStarted = performance.now();
|
|
180
|
+
const selected = {
|
|
181
|
+
...environment,
|
|
182
|
+
WISCONSIN_CONTENT_REPO: contentRepo,
|
|
183
|
+
VITE_PUBLIC_EDITION: String(edition === 'public')
|
|
184
|
+
};
|
|
185
|
+
await command('bun', ['tooling/prepare.ts'], selected, abort.signal);
|
|
186
|
+
const search = path.join(staging, `search-${edition}`);
|
|
187
|
+
await command('node', ['tooling/search.js', search], selected, abort.signal);
|
|
188
|
+
editions.push(
|
|
189
|
+
packaging.captureRuntimeEdition(site, edition, path.join(staging, edition), search)
|
|
190
|
+
);
|
|
191
|
+
console.log(
|
|
192
|
+
`build: ${edition} content completed in ${((performance.now() - editionStarted) / 1000).toFixed(2)}s`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (
|
|
196
|
+
app.applicationCacheIdentity(site, environment).identity !== identity.identity ||
|
|
197
|
+
contentInputFingerprint(contentRepo, identity.identity) !== contentInputs
|
|
198
|
+
)
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Source or course content changed during the build; refusing mixed snapshots'
|
|
201
|
+
);
|
|
202
|
+
abort.signal.throwIfAborted();
|
|
203
|
+
const packed = packaging.packageRuntime(
|
|
204
|
+
site,
|
|
205
|
+
path.join(product, 'cloudflare'),
|
|
206
|
+
identity.applicationVersion,
|
|
207
|
+
editions,
|
|
208
|
+
appInventory
|
|
209
|
+
);
|
|
210
|
+
const config = readDeploymentConfig(site);
|
|
211
|
+
const worker = await loadPreparedWorker(path.join(product, 'prepared-worker'), config);
|
|
212
|
+
writeFileSync(
|
|
213
|
+
path.join(product, 'deployment.json'),
|
|
214
|
+
JSON.stringify(buildDeploymentSnapshot(worker, packed.inventory, config, packed.provenance))
|
|
215
|
+
);
|
|
216
|
+
if (
|
|
217
|
+
app.applicationCacheIdentity(site, environment).identity !== identity.identity ||
|
|
218
|
+
contentInputFingerprint(contentRepo, identity.identity) !== contentInputs
|
|
219
|
+
)
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Source or course content changed during packaging; refusing mixed snapshots'
|
|
222
|
+
);
|
|
223
|
+
abort.signal.throwIfAborted();
|
|
224
|
+
packaging.promoteRuntimeProduct(site, product);
|
|
225
|
+
flushAssetHashCache(site);
|
|
226
|
+
console.log(
|
|
227
|
+
`publishing: ${packed.publicRoutes} public SSR routes; edition data remains private`
|
|
228
|
+
);
|
|
229
|
+
console.log(`build: all completed in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
230
|
+
} finally {
|
|
231
|
+
if (staging) rmSync(staging, { recursive: true, force: true });
|
|
232
|
+
process.removeListener('SIGINT', interrupt);
|
|
233
|
+
process.removeListener('SIGTERM', interrupt);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function serialBuild() {
|
|
238
|
+
const output = path.join(site, 'build/.svelte-kit/cloudflare');
|
|
239
|
+
const staged = path.join(site, 'build/generated/public-site');
|
|
240
|
+
const manifestPath = path.join(site, 'build/generated/public-assets.json');
|
|
241
|
+
let publicAssets: Record<string, string> = {};
|
|
242
|
+
mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
243
|
+
writeFileSync(manifestPath, '{}');
|
|
244
|
+
rmSync(staged, { recursive: true, force: true });
|
|
245
|
+
|
|
246
|
+
const started = performance.now();
|
|
247
|
+
|
|
248
|
+
function build(publicEdition: boolean) {
|
|
249
|
+
const started = performance.now();
|
|
250
|
+
const result = spawnSync('bun', ['x', 'vite', 'build'], {
|
|
251
|
+
cwd: site,
|
|
252
|
+
stdio: 'inherit',
|
|
253
|
+
env: {
|
|
254
|
+
...process.env,
|
|
255
|
+
NODE_ENV: process.env.NODE_ENV ?? 'production',
|
|
256
|
+
VITE_PUBLIC_EDITION: String(publicEdition)
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (result.status !== 0)
|
|
260
|
+
throw new Error(`${publicEdition ? 'Public' : 'Full'} build failed`, {
|
|
261
|
+
cause: result.error
|
|
262
|
+
});
|
|
263
|
+
console.log(
|
|
264
|
+
`build: ${publicEdition ? 'public' : 'full'} completed in ${((performance.now() - started) / 1000).toFixed(2)}s`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
{
|
|
269
|
+
build(true);
|
|
270
|
+
const pages = JSON.parse(
|
|
271
|
+
readFileSync(path.join(site, 'build/generated/public-routes.json'), 'utf8')
|
|
272
|
+
);
|
|
273
|
+
const content = JSON.parse(
|
|
274
|
+
readFileSync(path.join(site, 'build/generated/content-manifest.json'), 'utf8')
|
|
275
|
+
);
|
|
276
|
+
for (const asset of content.htmlAssets) {
|
|
277
|
+
pages[`/${asset}`] = `${asset}.html`;
|
|
278
|
+
pages[`/${asset}.html`] = `${asset}.html`;
|
|
279
|
+
}
|
|
280
|
+
const fileEntries = JSON.parse(
|
|
281
|
+
readFileSync(path.join(site, 'src/lib/generated/file-entries.json'), 'utf8')
|
|
282
|
+
);
|
|
283
|
+
publicAssets = publicAssetManifest(output, pages, fileEntries);
|
|
284
|
+
// Both editions live on the same filesystem. Move the public tree aside
|
|
285
|
+
// before the full adapter recreates its output, without copying every asset.
|
|
286
|
+
renameSync(output, staged);
|
|
287
|
+
console.log(
|
|
288
|
+
`publishing: ${Object.keys(pages).length} public pages; ${fileEntries.length} file routes share one shell; history remains private`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
build(false);
|
|
292
|
+
renameSync(staged, path.join(output, '_published'));
|
|
293
|
+
writeFileSync(manifestPath, JSON.stringify(publicAssets));
|
|
294
|
+
|
|
295
|
+
console.log(`build: all completed in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function isolatedBuild(mode: 'parallel' | 'isolated-serial') {
|
|
299
|
+
const started = performance.now();
|
|
300
|
+
const abort = new AbortController();
|
|
301
|
+
const interrupt = () => abort.abort(new Error('Build interrupted by SIGINT'));
|
|
302
|
+
const terminate = () => abort.abort(new Error('Build interrupted by SIGTERM'));
|
|
303
|
+
process.on('SIGINT', interrupt);
|
|
304
|
+
process.on('SIGTERM', terminate);
|
|
305
|
+
let workspace:
|
|
306
|
+
| { publicSite: string; fullSite: string; cleanup: () => void | Promise<void> }
|
|
307
|
+
| undefined;
|
|
308
|
+
let cleanupSeconds = 0;
|
|
309
|
+
let succeeded = false;
|
|
310
|
+
try {
|
|
311
|
+
const [
|
|
312
|
+
{ createBuildWorkspaces, mergeBuildCaches },
|
|
313
|
+
{ assembleBuilds },
|
|
314
|
+
{ applicationVersion },
|
|
315
|
+
{ contentInputFingerprint }
|
|
316
|
+
] = await Promise.all([
|
|
317
|
+
import('./lib/build-workspace'),
|
|
318
|
+
import('./lib/build-assembly'),
|
|
319
|
+
import('./lib/app-version.js'),
|
|
320
|
+
import('./lib/dev-state')
|
|
321
|
+
]);
|
|
322
|
+
const contentRepo = resolveProject(site).root;
|
|
323
|
+
const versionEnvironment: NodeJS.ProcessEnv = {
|
|
324
|
+
...process.env,
|
|
325
|
+
NODE_ENV: process.env.NODE_ENV ?? 'production'
|
|
326
|
+
};
|
|
327
|
+
delete versionEnvironment.WISCONSIN_APPLICATION_VERSION;
|
|
328
|
+
const version = applicationVersion(site, versionEnvironment);
|
|
329
|
+
const contentInputs = contentInputFingerprint(contentRepo, version);
|
|
330
|
+
abort.signal.throwIfAborted();
|
|
331
|
+
workspace = await createBuildWorkspaces(site, contentRepo);
|
|
332
|
+
abort.signal.throwIfAborted();
|
|
333
|
+
const benchmarks = path.join(site, 'build/benchmarks');
|
|
334
|
+
mkdirSync(benchmarks, { recursive: true, mode: 0o700 });
|
|
335
|
+
const logDirectory = mkdtempSync(path.join(benchmarks, 'edition-'));
|
|
336
|
+
const setupSeconds = (performance.now() - started) / 1000;
|
|
337
|
+
console.log(`build: ${mode} setup in ${setupSeconds.toFixed(2)}s; logs: ${logDirectory}`);
|
|
338
|
+
const buildStarted = performance.now();
|
|
339
|
+
const editions = await runEditionBuilds(workspace, {
|
|
340
|
+
mode,
|
|
341
|
+
contentRepo,
|
|
342
|
+
applicationVersion: version,
|
|
343
|
+
logDirectory,
|
|
344
|
+
signal: abort.signal
|
|
345
|
+
});
|
|
346
|
+
const buildSpanSeconds = (performance.now() - buildStarted) / 1000;
|
|
347
|
+
abort.signal.throwIfAborted();
|
|
348
|
+
const validationStarted = performance.now();
|
|
349
|
+
if (
|
|
350
|
+
applicationVersion(site, versionEnvironment) !== version ||
|
|
351
|
+
contentInputFingerprint(contentRepo, version) !== contentInputs
|
|
352
|
+
)
|
|
353
|
+
throw new Error(
|
|
354
|
+
'Source or course content changed during the build; refusing to assemble mixed editions'
|
|
355
|
+
);
|
|
356
|
+
const validationSeconds = (performance.now() - validationStarted) / 1000;
|
|
357
|
+
const mergeStarted = performance.now();
|
|
358
|
+
mergeBuildCaches(
|
|
359
|
+
workspace.publicSite,
|
|
360
|
+
workspace.fullSite,
|
|
361
|
+
path.join(workspace.fullSite, 'build/generated/cache')
|
|
362
|
+
);
|
|
363
|
+
const cacheMergeSeconds = (performance.now() - mergeStarted) / 1000;
|
|
364
|
+
const assemblyStarted = performance.now();
|
|
365
|
+
const publication = assembleBuilds(site, workspace.publicSite, workspace.fullSite);
|
|
366
|
+
const assemblySeconds = (performance.now() - assemblyStarted) / 1000;
|
|
367
|
+
console.log(
|
|
368
|
+
`publishing: ${publication.publicPages} public pages; ${publication.fileRoutes} file routes share one shell; history remains private`
|
|
369
|
+
);
|
|
370
|
+
const cleanupStarted = performance.now();
|
|
371
|
+
await workspace.cleanup();
|
|
372
|
+
workspace = undefined;
|
|
373
|
+
cleanupSeconds = (performance.now() - cleanupStarted) / 1000;
|
|
374
|
+
const totalSeconds = (performance.now() - started) / 1000;
|
|
375
|
+
console.log(`build: all completed in ${totalSeconds.toFixed(2)}s`);
|
|
376
|
+
console.log(
|
|
377
|
+
`build: metrics ${JSON.stringify({
|
|
378
|
+
mode,
|
|
379
|
+
setupSeconds,
|
|
380
|
+
...editions,
|
|
381
|
+
buildSpanSeconds,
|
|
382
|
+
validationSeconds,
|
|
383
|
+
cacheMergeSeconds,
|
|
384
|
+
assemblySeconds,
|
|
385
|
+
cleanupSeconds,
|
|
386
|
+
totalSeconds,
|
|
387
|
+
logs: {
|
|
388
|
+
public: path.join(logDirectory, 'public.log'),
|
|
389
|
+
full: path.join(logDirectory, 'full.log')
|
|
390
|
+
}
|
|
391
|
+
})}`
|
|
392
|
+
);
|
|
393
|
+
succeeded = true;
|
|
394
|
+
} finally {
|
|
395
|
+
if (workspace) {
|
|
396
|
+
const cleanupStarted = performance.now();
|
|
397
|
+
await workspace.cleanup();
|
|
398
|
+
cleanupSeconds = (performance.now() - cleanupStarted) / 1000;
|
|
399
|
+
}
|
|
400
|
+
process.removeListener('SIGINT', interrupt);
|
|
401
|
+
process.removeListener('SIGTERM', terminate);
|
|
402
|
+
if (!succeeded)
|
|
403
|
+
console.log(
|
|
404
|
+
`build: ${mode} failed after ${((performance.now() - started) / 1000).toFixed(2)}s; cleanup ${cleanupSeconds.toFixed(2)}s`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { compilerCacheFiles } from './lib/cache-selection';
|
|
15
|
+
|
|
16
|
+
const mode = process.argv[2];
|
|
17
|
+
if (mode === 'plan' || mode === 'save-products' || mode === 'restore-products') {
|
|
18
|
+
const { planProductCaches, transferProductCaches } = await import('./lib/product-cache');
|
|
19
|
+
const { resolve } = await import('node:path');
|
|
20
|
+
const options: import('./lib/product-cache').ProductOptions = {};
|
|
21
|
+
let planFile: string | undefined;
|
|
22
|
+
for (let i = 3; i < process.argv.length; i++) {
|
|
23
|
+
const argument = process.argv[i];
|
|
24
|
+
if (argument === '--restore') options.restore = true;
|
|
25
|
+
else if (argument === '--group') options.group = process.argv[++i];
|
|
26
|
+
else if (argument === '--namespace') {
|
|
27
|
+
const namespace = process.argv[++i];
|
|
28
|
+
if (!['production', 'benchmark'].includes(namespace))
|
|
29
|
+
throw new Error('Invalid product namespace');
|
|
30
|
+
options.namespace = namespace as import('./lib/product-cache').ProductNamespace;
|
|
31
|
+
} else if (argument === '--kind') {
|
|
32
|
+
const kind = process.argv[++i];
|
|
33
|
+
if (!['global', 'course', 'search', 'application'].includes(kind))
|
|
34
|
+
throw new Error('Invalid product kind');
|
|
35
|
+
options.kind = kind as import('./lib/product-cache').ProductKind;
|
|
36
|
+
} else if (argument === '--plan-file') planFile = process.argv[++i];
|
|
37
|
+
else throw new Error('Invalid product-cache argument');
|
|
38
|
+
}
|
|
39
|
+
const result =
|
|
40
|
+
mode === 'plan'
|
|
41
|
+
? await planProductCaches(process.cwd(), options)
|
|
42
|
+
: await transferProductCaches(
|
|
43
|
+
process.cwd(),
|
|
44
|
+
mode === 'save-products' ? 'save' : 'restore',
|
|
45
|
+
process.env.BUILD_CACHE_KEY,
|
|
46
|
+
options
|
|
47
|
+
);
|
|
48
|
+
const json = JSON.stringify(result);
|
|
49
|
+
if (planFile) {
|
|
50
|
+
mkdirSync(path.dirname(resolve(planFile)), { recursive: true });
|
|
51
|
+
writeFileSync(resolve(planFile), json + '\n', { mode: 0o600 });
|
|
52
|
+
} else console.log(json);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
if (mode !== 'restore' && mode !== 'save') throw new Error('Expected restore or save');
|
|
56
|
+
const secret = process.env.BUILD_CACHE_KEY;
|
|
57
|
+
const cache = path.resolve('build/generated/cache');
|
|
58
|
+
const encrypted = path.resolve('build/generated/compiler-cache.gpg');
|
|
59
|
+
if (!secret) {
|
|
60
|
+
console.log(
|
|
61
|
+
mode === 'restore'
|
|
62
|
+
? 'BUILD_CACHE_KEY is missing; building cold'
|
|
63
|
+
: 'BUILD_CACHE_KEY is missing; skipping cache save'
|
|
64
|
+
);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
if (mode === 'restore' && !existsSync(encrypted)) {
|
|
68
|
+
console.log('No compatible cache archive was restored; building cold');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const temporary = mkdtempSync(path.join(tmpdir(), 'wisconsin-cache-'));
|
|
73
|
+
const archive = path.join(temporary, 'cache.tar.gz');
|
|
74
|
+
const status = path.join(temporary, 'status');
|
|
75
|
+
const env = { ...process.env, GNUPGHOME: temporary };
|
|
76
|
+
const run = (command: string, args: string[]) =>
|
|
77
|
+
execFileSync(command, args, {
|
|
78
|
+
env,
|
|
79
|
+
input: command === 'gpg' ? secret + '\n' : undefined,
|
|
80
|
+
stdio: 'pipe'
|
|
81
|
+
});
|
|
82
|
+
const gpg = [
|
|
83
|
+
'--batch',
|
|
84
|
+
'--yes',
|
|
85
|
+
'--pinentry-mode',
|
|
86
|
+
'loopback',
|
|
87
|
+
'--no-symkey-cache',
|
|
88
|
+
'--passphrase-fd',
|
|
89
|
+
'0'
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (mode === 'restore') {
|
|
94
|
+
run('gpg', [...gpg, '--status-file', status, '--output', archive, '--decrypt', encrypted]);
|
|
95
|
+
if (!readFileSync(status, 'utf8').split('\n').includes('[GNUPG:] GOODMDC'))
|
|
96
|
+
throw new Error('Cache integrity was not verified');
|
|
97
|
+
const staged = path.join(temporary, 'unpacked');
|
|
98
|
+
mkdirSync(staged);
|
|
99
|
+
run('tar', ['-xzf', archive, '-C', staged, '--no-same-owner']);
|
|
100
|
+
mkdirSync(cache, { recursive: true });
|
|
101
|
+
cpSync(staged, cache, { recursive: true });
|
|
102
|
+
console.log('Compiler cache archive restored; each stage validates its own inputs');
|
|
103
|
+
} else {
|
|
104
|
+
const files = compilerCacheFiles(cache);
|
|
105
|
+
const list = path.join(temporary, 'files');
|
|
106
|
+
writeFileSync(list, files.map((file) => file + '\0').join(''));
|
|
107
|
+
run('tar', ['-czf', archive, '-C', cache, '--null', '--files-from', list]);
|
|
108
|
+
run('gpg', [
|
|
109
|
+
...gpg,
|
|
110
|
+
'--symmetric',
|
|
111
|
+
'--cipher-algo',
|
|
112
|
+
'AES256',
|
|
113
|
+
'--compress-algo',
|
|
114
|
+
'none',
|
|
115
|
+
'--output',
|
|
116
|
+
encrypted,
|
|
117
|
+
archive
|
|
118
|
+
]);
|
|
119
|
+
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, 'ready=true\n');
|
|
120
|
+
console.log(`Compiler cache encrypted (${files.length} current files)`);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
console.log(
|
|
124
|
+
mode === 'restore'
|
|
125
|
+
? 'Cache could not be verified; building cold'
|
|
126
|
+
: 'Cache encryption failed; skipping cache save'
|
|
127
|
+
);
|
|
128
|
+
} finally {
|
|
129
|
+
try {
|
|
130
|
+
run('gpgconf', ['--kill', 'gpg-agent']);
|
|
131
|
+
} catch {}
|
|
132
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
133
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { resolveProject } from './lib/project-config';
|
|
2
|
+
import { markdownFiles } from './lib/markdown-files';
|
|
3
|
+
export { markdownFiles } from './lib/markdown-files';
|
|
4
|
+
import { preserveCurrency } from './lib/currency';
|
|
5
|
+
import { stripObsidianComments } from './lib/comments';
|
|
6
|
+
/** Validate Markdown math and render Mermaid with the site's installed engines. */
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { unified } from 'unified';
|
|
11
|
+
import remarkParse from 'remark-parse';
|
|
12
|
+
import remarkGfm from 'remark-gfm';
|
|
13
|
+
import remarkMath from 'remark-math';
|
|
14
|
+
import { visit } from 'unist-util-visit';
|
|
15
|
+
import katex, { type KatexOptions } from 'katex';
|
|
16
|
+
import { chromium, type Browser, type Page } from '@playwright/test';
|
|
17
|
+
|
|
18
|
+
export interface Expression {
|
|
19
|
+
kind: 'math' | 'mermaid';
|
|
20
|
+
value: string;
|
|
21
|
+
line: number;
|
|
22
|
+
column: number;
|
|
23
|
+
display: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function expressions(markdown: string): Expression[] {
|
|
27
|
+
// Keep original line/column positions while excluding metadata and OFM comments.
|
|
28
|
+
const blank = (s: string) => s.replace(/[^\r\n]/g, ' ');
|
|
29
|
+
const source = stripObsidianComments(
|
|
30
|
+
markdown.replace(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)(?:\r?\n|$)/, blank),
|
|
31
|
+
true
|
|
32
|
+
);
|
|
33
|
+
const tree = unified().use(remarkParse).use(remarkGfm).use(remarkMath).parse(source);
|
|
34
|
+
preserveCurrency(source)(tree);
|
|
35
|
+
const result: Expression[] = [];
|
|
36
|
+
visit(tree, (node) => {
|
|
37
|
+
if (
|
|
38
|
+
node.type === 'math' ||
|
|
39
|
+
node.type === 'inlineMath' ||
|
|
40
|
+
(node.type === 'code' && node.lang === 'mermaid')
|
|
41
|
+
) {
|
|
42
|
+
result.push({
|
|
43
|
+
kind: node.type === 'code' ? 'mermaid' : 'math',
|
|
44
|
+
value: node.value,
|
|
45
|
+
line: node.position?.start.line ?? 1,
|
|
46
|
+
column: node.position?.start.column ?? 1,
|
|
47
|
+
display: node.type === 'math'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function checkMath(
|
|
55
|
+
expression: Expression,
|
|
56
|
+
macros: NonNullable<KatexOptions['macros']> = {}
|
|
57
|
+
): void {
|
|
58
|
+
katex.renderToString(expression.value, {
|
|
59
|
+
throwOnError: true,
|
|
60
|
+
displayMode: expression.display,
|
|
61
|
+
output: 'html',
|
|
62
|
+
macros,
|
|
63
|
+
strict: 'ignore' // match the site's permissive rendering of supported extensions
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function mermaidPage(browser: Browser): Promise<Page> {
|
|
68
|
+
const page = await browser.newPage();
|
|
69
|
+
// Notes need no network access to render. Keep any diagram links local/inert.
|
|
70
|
+
await page.route('**/*', (route) => route.abort());
|
|
71
|
+
await page.setContent('<!doctype html><html><body></body></html>');
|
|
72
|
+
const entry = import.meta.resolve('mermaid');
|
|
73
|
+
await page.addScriptTag({
|
|
74
|
+
path: path.join(path.dirname(fileURLToPath(entry)), 'mermaid.min.js')
|
|
75
|
+
});
|
|
76
|
+
return page;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function checkMermaid(page: Page, source: string): Promise<void> {
|
|
80
|
+
await page.evaluate(async (text) => {
|
|
81
|
+
const mermaid = (window as unknown as { mermaid: typeof import('mermaid').default }).mermaid;
|
|
82
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: 'loose', theme: 'base' });
|
|
83
|
+
await mermaid.parse(text);
|
|
84
|
+
const { svg } = await mermaid.render('validation-diagram', text);
|
|
85
|
+
if (!svg.includes('<svg')) throw new Error('Mermaid produced no SVG');
|
|
86
|
+
document.body.replaceChildren();
|
|
87
|
+
}, source);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
if (args.includes('--help')) {
|
|
93
|
+
console.log(
|
|
94
|
+
'Usage: bun run check:diagrams [Markdown files or Git directories...]\nDefaults to all content, including untracked non-ignored notes.'
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const content = resolveProject().contentDir;
|
|
99
|
+
const files = markdownFiles(args.length ? args : [content]);
|
|
100
|
+
if (!files.length) throw new Error('No Markdown files found');
|
|
101
|
+
let math = 0,
|
|
102
|
+
mermaid = 0,
|
|
103
|
+
failures = 0;
|
|
104
|
+
let browser: Browser | undefined;
|
|
105
|
+
let page: Page | undefined;
|
|
106
|
+
try {
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const macros: NonNullable<KatexOptions['macros']> = {};
|
|
109
|
+
for (const expression of expressions(readFileSync(file, 'utf8'))) {
|
|
110
|
+
if (expression.kind === 'mermaid' && !browser) {
|
|
111
|
+
// Browser setup failures are infrastructure errors, not errors in every note.
|
|
112
|
+
browser = await chromium.launch();
|
|
113
|
+
page = await mermaidPage(browser);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
if (expression.kind === 'math') {
|
|
117
|
+
math++;
|
|
118
|
+
checkMath(expression, macros);
|
|
119
|
+
} else {
|
|
120
|
+
mermaid++;
|
|
121
|
+
// Bound pathological diagrams; replace the page after any failure.
|
|
122
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
123
|
+
try {
|
|
124
|
+
await Promise.race([
|
|
125
|
+
checkMermaid(page!, expression.value),
|
|
126
|
+
new Promise<never>((_, reject) => {
|
|
127
|
+
timer = setTimeout(
|
|
128
|
+
() => reject(new Error('Mermaid render exceeded 15 seconds')),
|
|
129
|
+
15_000
|
|
130
|
+
);
|
|
131
|
+
})
|
|
132
|
+
]);
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
failures++;
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
console.error(
|
|
141
|
+
`${path.relative(process.cwd(), file)}:${expression.line}:${expression.column}: ${expression.kind}: ${message.split(/\n\s+at /)[0].replace(/\s+/g, ' ')}`
|
|
142
|
+
);
|
|
143
|
+
if (expression.kind === 'mermaid') {
|
|
144
|
+
await page!.close();
|
|
145
|
+
page = await mermaidPage(browser!);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
await browser?.close();
|
|
152
|
+
}
|
|
153
|
+
console.log(
|
|
154
|
+
`Checked ${files.length} Markdown files: ${math} math expressions, ${mermaid} Mermaid diagrams; ${failures} errors.`
|
|
155
|
+
);
|
|
156
|
+
if (failures) process.exitCode = 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (import.meta.main) {
|
|
160
|
+
main().catch((error) => {
|
|
161
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
});
|
|
164
|
+
}
|