@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,112 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { deploymentError, readDeploymentConfig } from './lib/deployment-config';
|
|
4
|
+
import {
|
|
5
|
+
captureAssetInventory,
|
|
6
|
+
assetPath,
|
|
7
|
+
SHA256,
|
|
8
|
+
readCurrentDescriptorProvenance,
|
|
9
|
+
assetContentType
|
|
10
|
+
} from './lib/deployment-manifest';
|
|
11
|
+
import {
|
|
12
|
+
buildDeploymentSnapshot,
|
|
13
|
+
loadPreparedWorker,
|
|
14
|
+
prepareWorker,
|
|
15
|
+
type DeploymentSnapshot
|
|
16
|
+
} from './lib/deployment-prepare';
|
|
17
|
+
import { CloudflareDeploymentAPI, uploadDeployment } from './lib/deployment-upload';
|
|
18
|
+
import { captureAssetHashes, flushAssetHashCache } from './lib/asset-hash-cache';
|
|
19
|
+
|
|
20
|
+
/** No implicit network operation: --prepare is offline; --upload is an explicit production mutation. */
|
|
21
|
+
export async function deploymentCLI(args: string[]): Promise<void> {
|
|
22
|
+
const options = new Map<string, string>();
|
|
23
|
+
let mode: '--prepare' | '--upload' | undefined;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const argument = args[i];
|
|
26
|
+
if (argument === '--prepare' || argument === '--upload') {
|
|
27
|
+
if (mode) deploymentError('select exactly one deployment mode');
|
|
28
|
+
mode = argument;
|
|
29
|
+
} else if (
|
|
30
|
+
['--output', '--snapshot', '--provenance', '--site', '--worker'].includes(argument) &&
|
|
31
|
+
args[i + 1] &&
|
|
32
|
+
!args[i + 1].startsWith('--') &&
|
|
33
|
+
!options.has(argument)
|
|
34
|
+
)
|
|
35
|
+
options.set(argument, args[++i]);
|
|
36
|
+
else deploymentError('unsupported CLI arguments');
|
|
37
|
+
}
|
|
38
|
+
if (!mode) deploymentError('select --prepare or --upload explicitly');
|
|
39
|
+
const site = path.resolve(options.get('--site') ?? path.resolve(import.meta.dir, '..'));
|
|
40
|
+
const config = readDeploymentConfig(site);
|
|
41
|
+
const provenance = await readCurrentDescriptorProvenance(
|
|
42
|
+
path.resolve(site, config.assets.directory)
|
|
43
|
+
);
|
|
44
|
+
const requestedProvenance = options.get('--provenance');
|
|
45
|
+
if (
|
|
46
|
+
requestedProvenance &&
|
|
47
|
+
(!SHA256.test(requestedProvenance) || requestedProvenance !== provenance)
|
|
48
|
+
)
|
|
49
|
+
deploymentError('explicit provenance does not match current content descriptor');
|
|
50
|
+
const outputDirectory = path.resolve(
|
|
51
|
+
options.get('--output') ?? path.join(site, 'build/.svelte-kit/prepared-worker')
|
|
52
|
+
);
|
|
53
|
+
const snapshotFile = path.resolve(
|
|
54
|
+
options.get('--snapshot') ?? path.join(site, 'build/.svelte-kit/deployment.json')
|
|
55
|
+
);
|
|
56
|
+
if (mode === '--prepare') {
|
|
57
|
+
const worker = await prepareWorker(site, outputDirectory);
|
|
58
|
+
const inventory = await captureAssetInventory(
|
|
59
|
+
path.resolve(site, config.assets.directory),
|
|
60
|
+
provenance,
|
|
61
|
+
{
|
|
62
|
+
resolveAsset: async (absolute, filename) => ({
|
|
63
|
+
...captureAssetHashes(site, absolute),
|
|
64
|
+
contentType: assetContentType(filename)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
flushAssetHashCache(site);
|
|
69
|
+
const snapshot = buildDeploymentSnapshot(worker, inventory, config, provenance);
|
|
70
|
+
await writeFile(snapshotFile, JSON.stringify(snapshot), { mode: 0o600 });
|
|
71
|
+
console.log(
|
|
72
|
+
`deployment: prepared ${worker.modules.length} Worker modules and ${Object.keys(inventory.assets).length} assets offline`
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? config.account_id;
|
|
77
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
78
|
+
if (!accountId || !token)
|
|
79
|
+
deploymentError('explicit upload requires Cloudflare account and API credentials');
|
|
80
|
+
const workerDirectory = path.resolve(options.get('--worker') ?? outputDirectory);
|
|
81
|
+
const snapshot: DeploymentSnapshot = JSON.parse(await readFile(snapshotFile, 'utf8'));
|
|
82
|
+
if (snapshot.inventory.assets['/_content/current.json']?.sha256 !== provenance)
|
|
83
|
+
deploymentError('deployment inventory does not match current content descriptor');
|
|
84
|
+
const loadedWorker = await loadPreparedWorker(workerDirectory, config);
|
|
85
|
+
if (loadedWorker.digest !== snapshot.worker.digest)
|
|
86
|
+
deploymentError('Worker package does not match deployment snapshot');
|
|
87
|
+
await uploadDeployment(snapshot, {
|
|
88
|
+
api: new CloudflareDeploymentAPI(token),
|
|
89
|
+
accountId,
|
|
90
|
+
expectedProvenance: provenance,
|
|
91
|
+
config,
|
|
92
|
+
readModule: (module) => readFile(path.join(workerDirectory, module.filename)),
|
|
93
|
+
readAsset: (filename) => {
|
|
94
|
+
if (!assetPath(filename)) deploymentError('invalid asset path');
|
|
95
|
+
return readFile(path.join(path.resolve(site, config.assets.directory), filename.slice(1)));
|
|
96
|
+
},
|
|
97
|
+
log: console.log
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (import.meta.main) {
|
|
102
|
+
try {
|
|
103
|
+
await deploymentCLI(process.argv.slice(2));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(
|
|
106
|
+
error instanceof Error && error.message.startsWith('deployment:')
|
|
107
|
+
? error.message
|
|
108
|
+
: 'deployment: local preparation or input validation failed'
|
|
109
|
+
);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { isBuiltin } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { rolldown } from 'rolldown';
|
|
6
|
+
|
|
7
|
+
const site = path.resolve(import.meta.dirname, '..');
|
|
8
|
+
const socialAssets = ['assets/fonts/OverusedGrotesk-SemiBold.ttf', 'assets/social-background.svg'];
|
|
9
|
+
const rAssets = ['tooling/lib/knit-rmd.R'];
|
|
10
|
+
|
|
11
|
+
// Runtime reads and subprocess entrypoints are not visible in the import graph.
|
|
12
|
+
export const stages = {
|
|
13
|
+
parser: { entries: ['tooling/compiler.ts'], assets: [] },
|
|
14
|
+
history: { entries: ['tooling/lib/file-history.ts'], assets: [] },
|
|
15
|
+
gitdates: { entries: ['tooling/lib/lastmod.ts'], assets: [] },
|
|
16
|
+
social: { entries: ['tooling/lib/social-images.ts'], assets: socialAssets },
|
|
17
|
+
rmd: { entries: ['tooling/lib/rmd-previews.ts'], assets: rAssets },
|
|
18
|
+
files: {
|
|
19
|
+
entries: ['tooling/lib/course-files.ts', 'tooling/lib/history-worker.ts'],
|
|
20
|
+
assets: rAssets
|
|
21
|
+
},
|
|
22
|
+
pipeline: {
|
|
23
|
+
entries: ['tooling/pipeline.ts', 'tooling/lib/history-worker.ts'],
|
|
24
|
+
assets: [...socialAssets, ...rAssets]
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type Stage = keyof typeof stages;
|
|
29
|
+
|
|
30
|
+
/** Hash portable names and bytes; never inode, mtime, or absolute checkout paths. */
|
|
31
|
+
function hashFiles(root: string, stage: Stage, files: string[]) {
|
|
32
|
+
const hash = createHash('sha256').update(
|
|
33
|
+
JSON.stringify([
|
|
34
|
+
'compiler-stage-v1',
|
|
35
|
+
stage,
|
|
36
|
+
process.version,
|
|
37
|
+
process.versions.bun ?? '',
|
|
38
|
+
process.platform,
|
|
39
|
+
process.arch,
|
|
40
|
+
stages[stage]
|
|
41
|
+
])
|
|
42
|
+
);
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const bytes = readFileSync(path.join(root, file));
|
|
45
|
+
hash.update(JSON.stringify([file, bytes.length])).update(bytes);
|
|
46
|
+
}
|
|
47
|
+
return hash.digest('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function stageFingerprint(stage: Stage, root = site): Promise<string> {
|
|
51
|
+
const definition = stages[stage];
|
|
52
|
+
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
53
|
+
const packages = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
54
|
+
const build = await rolldown({
|
|
55
|
+
cwd: root,
|
|
56
|
+
input: definition.entries.map((file) => path.join(root, file)),
|
|
57
|
+
platform: 'node',
|
|
58
|
+
// Tooling uses relative imports and must scan before SvelteKit generates its config.
|
|
59
|
+
tsconfig: false,
|
|
60
|
+
treeshake: false,
|
|
61
|
+
external: (id) =>
|
|
62
|
+
isBuiltin(id) || packages.some((pkg) => id === pkg || id.startsWith(pkg + '/')),
|
|
63
|
+
onwarn(warning) {
|
|
64
|
+
throw new Error(`Cache dependency scan: ${warning.message}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
let sources: string[];
|
|
68
|
+
try {
|
|
69
|
+
// Generate in memory so Rolldown traverses imports; never execute or emit the bundle.
|
|
70
|
+
await build.generate({ format: 'esm' });
|
|
71
|
+
sources = (await build.watchFiles).map((file) =>
|
|
72
|
+
path.relative(root, file).split(path.sep).join('/')
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
await build.close();
|
|
76
|
+
}
|
|
77
|
+
const files = [
|
|
78
|
+
...new Set([...sources, ...definition.assets, 'bun.lock', 'package.json', 'tsconfig.json'])
|
|
79
|
+
].sort();
|
|
80
|
+
return hashFiles(root, stage, files);
|
|
81
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const directories = ['src', 'tooling', 'worker', 'assets', 'database'];
|
|
7
|
+
const configFiles = [
|
|
8
|
+
'package.json',
|
|
9
|
+
'bun.lock',
|
|
10
|
+
'svelte.config.js',
|
|
11
|
+
'vite.config.ts',
|
|
12
|
+
'tsconfig.json',
|
|
13
|
+
'wrangler.jsonc',
|
|
14
|
+
'_headers',
|
|
15
|
+
'_redirects'
|
|
16
|
+
];
|
|
17
|
+
// Bun can load these before Vite starts. Include every supported mode so a
|
|
18
|
+
// copied dotenv file changing during an isolated build invalidates its snapshot.
|
|
19
|
+
const environmentFiles = [
|
|
20
|
+
'.env',
|
|
21
|
+
'.env.local',
|
|
22
|
+
'.env.production',
|
|
23
|
+
'.env.production.local',
|
|
24
|
+
'.env.development',
|
|
25
|
+
'.env.development.local',
|
|
26
|
+
'.env.test',
|
|
27
|
+
'.env.test.local'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Identify application code, independent of checkout paths and course revisions.
|
|
32
|
+
* Generated navigation and file icon catalogs are loaded as separate modules,
|
|
33
|
+
* so course catalog changes do not change the application bundle's version.
|
|
34
|
+
* @param {string} site
|
|
35
|
+
* @param {NodeJS.ProcessEnv} environment
|
|
36
|
+
*/
|
|
37
|
+
export function applicationVersion(site, environment = process.env) {
|
|
38
|
+
// Isolated edition snapshots use the canonical maintained-source inventory.
|
|
39
|
+
const snapshotVersion = environment.WISCONSIN_APPLICATION_VERSION;
|
|
40
|
+
if (snapshotVersion !== undefined) {
|
|
41
|
+
if (!/^[a-f0-9]{64}$/.test(snapshotVersion))
|
|
42
|
+
throw new Error('WISCONSIN_APPLICATION_VERSION must be a SHA-256 application version');
|
|
43
|
+
return snapshotVersion;
|
|
44
|
+
}
|
|
45
|
+
const files = new Set(configFiles);
|
|
46
|
+
/** @param {string} relative */
|
|
47
|
+
function walk(relative) {
|
|
48
|
+
if (!existsSync(path.join(site, relative))) return;
|
|
49
|
+
for (const entry of readdirSync(path.join(site, relative), { withFileTypes: true })) {
|
|
50
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
51
|
+
const file = `${relative}/${entry.name}`;
|
|
52
|
+
// The pinned R renderer controls content artifacts, not the application bundle.
|
|
53
|
+
if (file === 'src/lib/generated' || file === 'tooling/r-render-profile.json') continue;
|
|
54
|
+
if (entry.isDirectory()) walk(file);
|
|
55
|
+
else if (entry.isFile()) files.add(file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const directory of directories) walk(directory);
|
|
59
|
+
// These maintained assets are also present in Git-free content-test copies.
|
|
60
|
+
files.add('static/.gitignore');
|
|
61
|
+
files.add('static/favicon.png');
|
|
62
|
+
walk('static/fonts');
|
|
63
|
+
try {
|
|
64
|
+
// Respect static/.gitignore: synced course blobs must not change the app
|
|
65
|
+
// version, while any additional maintained static asset should change it.
|
|
66
|
+
for (const file of execFileSync(
|
|
67
|
+
'git',
|
|
68
|
+
['-C', site, 'ls-files', '--cached', '--others', '--exclude-standard', '-z', '--', 'static'],
|
|
69
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
|
|
70
|
+
).split('\0')) {
|
|
71
|
+
if (file) files.add(file);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Scratch copies intentionally have no Git metadata.
|
|
75
|
+
}
|
|
76
|
+
for (const file of environmentFiles) if (existsSync(path.join(site, file))) files.add(file);
|
|
77
|
+
const compileEnvironment = Object.entries(environment)
|
|
78
|
+
.filter(
|
|
79
|
+
([key]) =>
|
|
80
|
+
key === 'NODE_ENV' ||
|
|
81
|
+
((key.startsWith('VITE_') || key.startsWith('PUBLIC_')) && key !== 'VITE_PUBLIC_EDITION')
|
|
82
|
+
)
|
|
83
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
84
|
+
const hash = createHash('sha256').update(JSON.stringify(['site-app-v1', compileEnvironment]));
|
|
85
|
+
for (const file of [...files].sort()) {
|
|
86
|
+
const absolute = path.join(site, file);
|
|
87
|
+
if (!existsSync(absolute)) continue;
|
|
88
|
+
const bytes = readFileSync(absolute);
|
|
89
|
+
hash.update(JSON.stringify([file, bytes.length])).update(bytes);
|
|
90
|
+
}
|
|
91
|
+
return hash.digest('hex');
|
|
92
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
/** Keep root-mounted SSR client assets absolute without enabling mutable relative SSR paths. */
|
|
4
|
+
export function applicationAssetUrls(
|
|
5
|
+
staticExport = process.env.VITE_STATIC_EXPORT === 'true'
|
|
6
|
+
): Plugin {
|
|
7
|
+
return {
|
|
8
|
+
name: 'wisconsin:application-asset-urls',
|
|
9
|
+
enforce: 'post',
|
|
10
|
+
config(config) {
|
|
11
|
+
if (staticExport) return;
|
|
12
|
+
const renderBuiltUrl = config.experimental?.renderBuiltUrl;
|
|
13
|
+
return {
|
|
14
|
+
experimental: {
|
|
15
|
+
renderBuiltUrl(filename, details) {
|
|
16
|
+
// Kit 2.65 forces Vite's base to './'. With paths.relative=false,
|
|
17
|
+
// its {relative:false} result becomes './_app/...' even though Vite
|
|
18
|
+
// resolves preload dependencies against the importing module URL.
|
|
19
|
+
// The documented renderBuiltUrl hook can emit the correct root URL.
|
|
20
|
+
if (!details.ssr && details.hostType === 'js') return `/${filename}`;
|
|
21
|
+
return renderBuiltUrl?.(filename, details);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
constants,
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
mkdtempSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { applicationVersion } from './app-version.js';
|
|
19
|
+
|
|
20
|
+
const schema = 1;
|
|
21
|
+
const requiredFiles = [
|
|
22
|
+
'worker.js',
|
|
23
|
+
'svelte-worker.js',
|
|
24
|
+
'prepared-worker/worker.json',
|
|
25
|
+
'application-assets.json'
|
|
26
|
+
];
|
|
27
|
+
type FileRecord = { path: string; bytes: number; sha256: string };
|
|
28
|
+
export type ApplicationIdentity = {
|
|
29
|
+
identity: string;
|
|
30
|
+
applicationVersion: string;
|
|
31
|
+
runtime: {
|
|
32
|
+
node: string;
|
|
33
|
+
bun: string;
|
|
34
|
+
platform: string;
|
|
35
|
+
architecture: string;
|
|
36
|
+
packages: [string, string][];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
type Manifest = ApplicationIdentity & { schema: number; files: FileRecord[] };
|
|
40
|
+
export type CachedApplication = { directory: string; manifest: Manifest };
|
|
41
|
+
const copyOptions = { recursive: true, mode: constants.COPYFILE_FICLONE, preserveTimestamps: true };
|
|
42
|
+
const digest = (bytes: string | Buffer) => createHash('sha256').update(bytes).digest('hex');
|
|
43
|
+
const within = (root: string, file: string) => file === root || file.startsWith(root + path.sep);
|
|
44
|
+
|
|
45
|
+
export function applicationEnvironment(
|
|
46
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
47
|
+
): NodeJS.ProcessEnv {
|
|
48
|
+
const selected: NodeJS.ProcessEnv = {
|
|
49
|
+
...environment,
|
|
50
|
+
NODE_ENV: environment.NODE_ENV ?? 'production',
|
|
51
|
+
VITE_PUBLIC_EDITION: 'false',
|
|
52
|
+
VITE_STATIC_EXPORT: 'false',
|
|
53
|
+
WISCONSIN_SKIP_CONTENT: '1'
|
|
54
|
+
};
|
|
55
|
+
delete selected.WISCONSIN_APPLICATION_VERSION;
|
|
56
|
+
return selected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read-only identity shared by the build producer and encrypted cache transport. */
|
|
60
|
+
export function applicationCacheIdentity(
|
|
61
|
+
site: string,
|
|
62
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
63
|
+
): ApplicationIdentity {
|
|
64
|
+
const selected = applicationEnvironment(environment);
|
|
65
|
+
const sourceVersion = applicationVersion(site, selected);
|
|
66
|
+
const declared = JSON.parse(readFileSync(path.join(site, 'package.json'), 'utf8'));
|
|
67
|
+
const packages: [string, string][] = Object.keys({
|
|
68
|
+
...declared.dependencies,
|
|
69
|
+
...declared.devDependencies
|
|
70
|
+
})
|
|
71
|
+
.sort()
|
|
72
|
+
.map((name) => {
|
|
73
|
+
try {
|
|
74
|
+
return [
|
|
75
|
+
name,
|
|
76
|
+
JSON.parse(readFileSync(path.join(site, 'node_modules', name, 'package.json'), 'utf8'))
|
|
77
|
+
.version
|
|
78
|
+
];
|
|
79
|
+
} catch {
|
|
80
|
+
return [name, 'missing'];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const node = spawnSync('node', ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
84
|
+
const runtime = {
|
|
85
|
+
node: node.status === 0 ? node.stdout.trim() : 'missing',
|
|
86
|
+
bun: process.versions.bun ?? 'missing',
|
|
87
|
+
platform: process.platform,
|
|
88
|
+
architecture: process.arch,
|
|
89
|
+
packages
|
|
90
|
+
};
|
|
91
|
+
const identity = digest(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
schema,
|
|
94
|
+
applicationVersion: sourceVersion,
|
|
95
|
+
runtime,
|
|
96
|
+
prerenderConcurrency: selected.WISCONSIN_PRERENDER_CONCURRENCY ?? '1'
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
return { identity, applicationVersion: sourceVersion, runtime };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inventory(directory: string): FileRecord[] {
|
|
103
|
+
const root = realpathSync(directory);
|
|
104
|
+
const files: FileRecord[] = [];
|
|
105
|
+
function walk(relative: string) {
|
|
106
|
+
for (const entry of readdirSync(path.join(root, relative), { withFileTypes: true })) {
|
|
107
|
+
const file = relative ? `${relative}/${entry.name}` : entry.name;
|
|
108
|
+
const absolute = path.join(root, file);
|
|
109
|
+
if (entry.isDirectory()) walk(file);
|
|
110
|
+
else {
|
|
111
|
+
if (!entry.isFile() || realpathSync(absolute) !== absolute)
|
|
112
|
+
throw new Error('Application cache contains a non-regular file');
|
|
113
|
+
const bytes = readFileSync(absolute);
|
|
114
|
+
files.push({ path: file, bytes: bytes.length, sha256: digest(bytes) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
walk('');
|
|
119
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cacheDirectory(site: string, identity: ApplicationIdentity) {
|
|
123
|
+
if (!/^[a-f0-9]{64}$/.test(identity.identity))
|
|
124
|
+
throw new Error('Invalid application cache identity');
|
|
125
|
+
return path.join(site, 'build/generated/cache/application', identity.identity);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function loadApplicationCache(
|
|
129
|
+
site: string,
|
|
130
|
+
identity: ApplicationIdentity
|
|
131
|
+
): CachedApplication | undefined {
|
|
132
|
+
const directory = cacheDirectory(site, identity);
|
|
133
|
+
try {
|
|
134
|
+
if (realpathSync(directory) !== path.resolve(directory))
|
|
135
|
+
throw new Error('Redirected application cache');
|
|
136
|
+
const manifest: Manifest = JSON.parse(
|
|
137
|
+
readFileSync(path.join(directory, 'manifest.json'), 'utf8')
|
|
138
|
+
);
|
|
139
|
+
if (
|
|
140
|
+
manifest.schema !== schema ||
|
|
141
|
+
manifest.identity !== identity.identity ||
|
|
142
|
+
manifest.applicationVersion !== identity.applicationVersion ||
|
|
143
|
+
JSON.stringify(manifest.runtime) !== JSON.stringify(identity.runtime) ||
|
|
144
|
+
!Array.isArray(manifest.files)
|
|
145
|
+
)
|
|
146
|
+
return;
|
|
147
|
+
const actual = inventory(path.join(directory, 'kit'));
|
|
148
|
+
if (JSON.stringify(actual) !== JSON.stringify(manifest.files)) return;
|
|
149
|
+
for (const required of requiredFiles)
|
|
150
|
+
if (!actual.some((file) => file.path === required)) return;
|
|
151
|
+
return { directory, manifest };
|
|
152
|
+
} catch {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Save a complete neutral prepared app, without any course content or search index. */
|
|
158
|
+
export function saveApplicationCache(
|
|
159
|
+
site: string,
|
|
160
|
+
identity: ApplicationIdentity
|
|
161
|
+
): CachedApplication {
|
|
162
|
+
const source = path.join(site, 'build/.svelte-kit');
|
|
163
|
+
const files = inventory(source);
|
|
164
|
+
for (const required of requiredFiles)
|
|
165
|
+
if (!files.some((file) => file.path === required))
|
|
166
|
+
throw new Error('Application artifact is incomplete');
|
|
167
|
+
if (
|
|
168
|
+
files.some((file) =>
|
|
169
|
+
/^cloudflare\/(?:_content|_published|pagefind|_files)(?:\/|$)/.test(file.path)
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
throw new Error('Application artifact contains generated content');
|
|
173
|
+
const directory = cacheDirectory(site, identity);
|
|
174
|
+
mkdirSync(path.dirname(directory), { recursive: true });
|
|
175
|
+
const transaction = mkdtempSync(path.join(path.dirname(directory), '.application-save-'));
|
|
176
|
+
const staged = path.join(transaction, 'application');
|
|
177
|
+
const backup = path.join(transaction, 'previous');
|
|
178
|
+
let moved = false,
|
|
179
|
+
preserve = false;
|
|
180
|
+
try {
|
|
181
|
+
mkdirSync(staged);
|
|
182
|
+
cpSync(source, path.join(staged, 'kit'), copyOptions);
|
|
183
|
+
const manifest: Manifest = { ...identity, schema, files };
|
|
184
|
+
writeFileSync(path.join(staged, 'manifest.json'), JSON.stringify(manifest));
|
|
185
|
+
if (existsSync(directory)) {
|
|
186
|
+
renameSync(directory, backup);
|
|
187
|
+
moved = true;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
renameSync(staged, directory);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (moved)
|
|
193
|
+
try {
|
|
194
|
+
renameSync(backup, directory);
|
|
195
|
+
} catch (rollback) {
|
|
196
|
+
preserve = true;
|
|
197
|
+
throw new AggregateError(
|
|
198
|
+
[error, rollback],
|
|
199
|
+
'Previous application cache preserved for recovery'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
const pointer = {
|
|
205
|
+
schema,
|
|
206
|
+
identity: identity.identity,
|
|
207
|
+
files: [
|
|
208
|
+
`application/${identity.identity}/manifest.json`,
|
|
209
|
+
...files.map((file) => `application/${identity.identity}/kit/${file.path}`)
|
|
210
|
+
]
|
|
211
|
+
};
|
|
212
|
+
const current = path.join(site, 'build/generated/cache/application-current.json');
|
|
213
|
+
const temporary = path.join(transaction, 'current.json');
|
|
214
|
+
writeFileSync(temporary, JSON.stringify(pointer));
|
|
215
|
+
renameSync(temporary, current);
|
|
216
|
+
return { directory, manifest };
|
|
217
|
+
} finally {
|
|
218
|
+
if (!preserve) rmSync(transaction, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function stageApplication(cache: CachedApplication, destination: string) {
|
|
223
|
+
if (existsSync(destination)) throw new Error('Application staging destination already exists');
|
|
224
|
+
cpSync(path.join(cache.directory, 'kit'), destination, copyOptions);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Prevent generated course assets from becoming inputs to the neutral Vite build. */
|
|
228
|
+
export function prepareApplicationStatic(site: string, destination: string) {
|
|
229
|
+
if (existsSync(destination))
|
|
230
|
+
throw new Error('Application static staging destination already exists');
|
|
231
|
+
mkdirSync(destination, { recursive: true });
|
|
232
|
+
let files: string[];
|
|
233
|
+
try {
|
|
234
|
+
files = execFileSync(
|
|
235
|
+
'git',
|
|
236
|
+
['-C', site, 'ls-files', '--cached', '--others', '--exclude-standard', '-z', '--', 'static'],
|
|
237
|
+
{ encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }
|
|
238
|
+
)
|
|
239
|
+
.split('\0')
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
} catch {
|
|
242
|
+
files = ['static/.gitignore', 'static/favicon.png'];
|
|
243
|
+
const fonts = path.join(site, 'static/fonts');
|
|
244
|
+
if (existsSync(fonts))
|
|
245
|
+
for (const entry of readdirSync(fonts, { recursive: true, withFileTypes: true }))
|
|
246
|
+
if (entry.isFile())
|
|
247
|
+
files.push(path.relative(site, path.join(entry.parentPath, entry.name)));
|
|
248
|
+
}
|
|
249
|
+
// Packaged runtimes often live under a Git-ignored .twiki directory; an empty
|
|
250
|
+
// git inventory there does not mean the engine's maintained fonts disappeared.
|
|
251
|
+
files.push('static/.gitignore', 'static/favicon.png');
|
|
252
|
+
const fonts = path.join(site, 'static/fonts');
|
|
253
|
+
if (existsSync(fonts))
|
|
254
|
+
for (const entry of readdirSync(fonts, { recursive: true, withFileTypes: true }))
|
|
255
|
+
if (entry.isFile()) files.push(path.relative(site, path.join(entry.parentPath, entry.name)));
|
|
256
|
+
const root = realpathSync(site);
|
|
257
|
+
for (const file of new Set(files)) {
|
|
258
|
+
const source = path.resolve(site, file);
|
|
259
|
+
if (!file.startsWith('static/') || !within(root, source))
|
|
260
|
+
throw new Error('Invalid maintained static path');
|
|
261
|
+
const info = lstatSync(source, { throwIfNoEntry: false });
|
|
262
|
+
if (!info) continue;
|
|
263
|
+
if (!info.isFile() || !within(root, realpathSync(source)))
|
|
264
|
+
throw new Error('Maintained static asset must be an owned regular file');
|
|
265
|
+
const target = path.join(destination, file.slice('static/'.length));
|
|
266
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
267
|
+
cpSync(source, target, copyOptions);
|
|
268
|
+
}
|
|
269
|
+
}
|