@wxxb789/anc 0.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/astro.config.mjs +255 -0
- package/bin/anc.mjs +798 -0
- package/package.json +93 -0
- package/public/_headers +45 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/fonts/Temml.woff2 +0 -0
- package/scripts/build-snapshot.js +69 -0
- package/scripts/build-wasm.js +67 -0
- package/scripts/build-workspace.js +50 -0
- package/scripts/copy-snapshot.js +48 -0
- package/scripts/copy-wasm.js +57 -0
- package/scripts/emit-redirects.js +64 -0
- package/scripts/generate-corpus.js +421 -0
- package/scripts/init-repository.js +249 -0
- package/scripts/load-config.js +975 -0
- package/scripts/markdown-to-artifact.js +1286 -0
- package/scripts/preview-site.js +364 -0
- package/scripts/publish-set-review.js +157 -0
- package/scripts/resolve-links.js +640 -0
- package/scripts/run-pagefind.js +125 -0
- package/scripts/scan-residue.js +1016 -0
- package/scripts/scan-secrets.js +268 -0
- package/scripts/seed-gitignore.js +410 -0
- package/scripts/snapshot-rows.js +205 -0
- package/scripts/validate-content.js +95 -0
- package/scripts/vendor-provenance.js +307 -0
- package/scripts/verify-output-inventory.js +307 -0
- package/scripts/write-report.js +351 -0
- package/scripts/write-snapshot.js +136 -0
- package/src/components/CollectionExplorer.astro +149 -0
- package/src/components/FacetIndex.astro +45 -0
- package/src/components/LinkedNotes.astro +94 -0
- package/src/components/NoteGraph.astro +434 -0
- package/src/components/NoteList.astro +54 -0
- package/src/components/SiteMetadata.astro +143 -0
- package/src/components/TableOfContents.astro +69 -0
- package/src/components/TagBrowser.astro +70 -0
- package/src/layouts/Layout.astro +305 -0
- package/src/lib/artifact-source.js +72 -0
- package/src/lib/build-bindings.js +11 -0
- package/src/lib/collection-navigation.js +145 -0
- package/src/lib/content.js +92 -0
- package/src/lib/diagram-mode.js +18 -0
- package/src/lib/graph-layout.js +219 -0
- package/src/lib/graph-selection.js +84 -0
- package/src/lib/graph.js +42 -0
- package/src/lib/link-resolution.js +389 -0
- package/src/lib/markdown.js +1469 -0
- package/src/lib/math-mode.js +31 -0
- package/src/lib/math-output.js +131 -0
- package/src/lib/math.js +165 -0
- package/src/lib/mermaid-environment.js +475 -0
- package/src/lib/mermaid-render.js +769 -0
- package/src/lib/preview-model.js +96 -0
- package/src/lib/relations.js +242 -0
- package/src/lib/rendered-marker.js +55 -0
- package/src/lib/route-path.js +192 -0
- package/src/lib/routes.js +342 -0
- package/src/lib/schema.js +578 -0
- package/src/lib/site.js +578 -0
- package/src/lib/snapshot-contract.js +216 -0
- package/src/lib/snapshot-operations.js +147 -0
- package/src/lib/snapshot-queries.js +216 -0
- package/src/lib/snapshot-reader.js +210 -0
- package/src/lib/snapshot.js +233 -0
- package/src/lib/sqlite-warning.js +22 -0
- package/src/lib/sqlite.js +14 -0
- package/src/lib/tag-browser-model.js +18 -0
- package/src/lib/translations.js +533 -0
- package/src/lib/wasm-asset.js +47 -0
- package/src/lib/worker-protocol.js +93 -0
- package/src/pages/404.astro +35 -0
- package/src/pages/about.astro +100 -0
- package/src/pages/collections/[slug].astro +36 -0
- package/src/pages/collections/index.astro +33 -0
- package/src/pages/graph.astro +64 -0
- package/src/pages/index.astro +33 -0
- package/src/pages/notes/[slug].astro +509 -0
- package/src/pages/privacy.astro +121 -0
- package/src/pages/private.astro +59 -0
- package/src/pages/recent.astro +33 -0
- package/src/pages/robots.txt.js +15 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +20 -0
- package/src/pages/tags/[tag].astro +56 -0
- package/src/pages/tags/index.astro +37 -0
- package/src/scripts/code-copy.js +45 -0
- package/src/scripts/diagram-disabled.js +16 -0
- package/src/scripts/diagram.js +322 -0
- package/src/scripts/graph-client.js +222 -0
- package/src/scripts/graph-draw.js +166 -0
- package/src/scripts/link-preview.js +334 -0
- package/src/scripts/math-disabled.js +18 -0
- package/src/scripts/math.js +133 -0
- package/src/scripts/preferences.js +123 -0
- package/src/scripts/search-dialog.js +651 -0
- package/src/scripts/snapshot-client.js +157 -0
- package/src/scripts/snapshot-worker.js +341 -0
- package/src/scripts/tag-browser.js +153 -0
- package/src/scripts/theme-init.js +35 -0
- package/src/scripts/toc-active.js +38 -0
- package/src/styles/code.css +152 -0
- package/src/styles/diagram.css +149 -0
- package/src/styles/global.css +1739 -0
- package/src/styles/graph.css +317 -0
- package/src/styles/math.css +376 -0
- package/src/styles/tokens.css +171 -0
- package/tsconfig.json +25 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wxxb789/anc",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Build a static, privacy-preserving site from a directory of Markdown",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"static-site-generator",
|
|
8
|
+
"static-site",
|
|
9
|
+
"markdown",
|
|
10
|
+
"obsidian",
|
|
11
|
+
"digital-garden",
|
|
12
|
+
"knowledge-base",
|
|
13
|
+
"backlinks",
|
|
14
|
+
"full-text-search",
|
|
15
|
+
"pagefind",
|
|
16
|
+
"privacy",
|
|
17
|
+
"self-hosted",
|
|
18
|
+
"astro",
|
|
19
|
+
"sqlite",
|
|
20
|
+
"wasm"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/wxxb789/anc.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/wxxb789/anc/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/wxxb789/anc#readme",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"packageManager": "pnpm@11.18.0",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.18.0"
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"anc": "bin/anc.mjs"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"astro.config.mjs",
|
|
46
|
+
"bin",
|
|
47
|
+
"public",
|
|
48
|
+
"scripts",
|
|
49
|
+
"src",
|
|
50
|
+
"tsconfig.json",
|
|
51
|
+
"!src/data/content.json",
|
|
52
|
+
"!scripts/build-fixture.ts",
|
|
53
|
+
"!scripts/compile-package.ts",
|
|
54
|
+
"!scripts/build-site.ts",
|
|
55
|
+
"!scripts/dist-lock.ts",
|
|
56
|
+
"!scripts/npm-command.ts",
|
|
57
|
+
"!scripts/benchmark-browser.ts",
|
|
58
|
+
"!scripts/benchmark-candidate.ts",
|
|
59
|
+
"!scripts/benchmark-corpus.ts",
|
|
60
|
+
"!scripts/benchmark-driver.ts",
|
|
61
|
+
"!scripts/benchmark-files.ts",
|
|
62
|
+
"!scripts/benchmark-host.ts",
|
|
63
|
+
"!scripts/benchmark-identity.ts",
|
|
64
|
+
"!scripts/benchmark-limits.ts",
|
|
65
|
+
"!scripts/benchmark-oracle.ts",
|
|
66
|
+
"!scripts/benchmark-package.ts",
|
|
67
|
+
"!scripts/benchmark-page.ts",
|
|
68
|
+
"!scripts/benchmark-server.ts",
|
|
69
|
+
"!scripts/benchmark-snapshot.ts",
|
|
70
|
+
"!scripts/benchmark-stats.ts",
|
|
71
|
+
"!scripts/benchmark-transfer.ts",
|
|
72
|
+
"!scripts/benchmark-workload-driver.ts",
|
|
73
|
+
"!scripts/benchmark-workload-report.ts",
|
|
74
|
+
"!scripts/benchmark-workload-ui.ts",
|
|
75
|
+
"!scripts/benchmark-workload.ts",
|
|
76
|
+
"!scripts/release-changelog.ts",
|
|
77
|
+
"!scripts/smoke-tarball.ts"
|
|
78
|
+
],
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@astrojs/prism": "4.1.2",
|
|
81
|
+
"@sqlite.org/sqlite-wasm": "3.53.4-build1",
|
|
82
|
+
"astro": "7.4.5",
|
|
83
|
+
"github-slugger": "2.0.0",
|
|
84
|
+
"happy-dom": "20.14.5",
|
|
85
|
+
"mermaid": "12.2.0",
|
|
86
|
+
"pagefind": "1.6.0",
|
|
87
|
+
"prismjs": "1.30.0",
|
|
88
|
+
"sanitize-html": "2.18.1",
|
|
89
|
+
"satteri": "0.11.6",
|
|
90
|
+
"temml": "0.14.0",
|
|
91
|
+
"yaml": "2.9.1"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/public/_headers
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The `style-src` value is coupled to `DIAGRAM_MODE` in `src/lib/diagram-mode.ts`
|
|
2
|
+
# and must be changed with it: `'self'` for build-time diagrams, which ship as
|
|
3
|
+
# markup, and `'self' 'unsafe-inline'` for client mode, whose renderer writes
|
|
4
|
+
# inline styles and an injected `<style>` element that this policy would
|
|
5
|
+
# otherwise block — producing a solid black rectangle where the diagram should
|
|
6
|
+
# be. `tests/math-and-diagrams.test.ts` and `tests/built-output.test.ts` both
|
|
7
|
+
# fail if the two disagree.
|
|
8
|
+
#
|
|
9
|
+
# Hash-pinning was measured and cannot replace the relaxation: a CSP hash never
|
|
10
|
+
# covers a `style=` attribute, so `'unsafe-inline'` survives in `style-src-attr`
|
|
11
|
+
# and is merely relocated. See the Mermaid feasibility probe, section 7.3.
|
|
12
|
+
/*
|
|
13
|
+
X-Content-Type-Options: nosniff
|
|
14
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
15
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
16
|
+
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'
|
|
17
|
+
|
|
18
|
+
# Everything Astro content-hashes. The hash is derived from the file's own
|
|
19
|
+
# bytes, so a changed file is a changed URL and a cached copy of the old one can
|
|
20
|
+
# never be stale — which is the only condition under which `immutable` is safe.
|
|
21
|
+
#
|
|
22
|
+
# The rule sets no header the `/*` block above sets. That matters on Cloudflare
|
|
23
|
+
# Pages, which joins same-named headers from every matching rule with a comma
|
|
24
|
+
# rather than choosing the most specific: two rules setting `Cache-Control`
|
|
25
|
+
# would emit both values. Overriding an inherited header needs an explicit
|
|
26
|
+
# `! Header-Name`. `tests/deployment.test.ts` gates exactly that.
|
|
27
|
+
/_astro/*
|
|
28
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
29
|
+
|
|
30
|
+
# Deliberately NOT listed: `/pagefind/*`.
|
|
31
|
+
#
|
|
32
|
+
# Only two of its four asset classes are content-hashed. `pagefind/index/*` and
|
|
33
|
+
# `pagefind/fragment/*` carry a hash; `pagefind-entry.json` and
|
|
34
|
+
# `wasm.*.pagefind` are stable-named and rewritten on every build. Marking the
|
|
35
|
+
# directory immutable would pin a returning reader's `pagefind-entry.json` to a
|
|
36
|
+
# year-old copy pointing at index chunks that no longer exist — search would
|
|
37
|
+
# break silently after the first republish, for exactly the readers who visit
|
|
38
|
+
# most often. The platform default (`public, max-age=0, must-revalidate` plus an
|
|
39
|
+
# `ETag`) revalidates cheaply and is correct for all four.
|
|
40
|
+
#
|
|
41
|
+
# Also not listed: HTML and the JSON data files. Requirements section 20 asks
|
|
42
|
+
# for a short cache or revalidation for HTML and a short cache with `ETag` for
|
|
43
|
+
# JSON, and Cloudflare Pages already serves precisely that by default on every
|
|
44
|
+
# 200. A rule restating the platform default would be one more thing to keep in
|
|
45
|
+
# sync with it.
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
2
|
+
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
3
|
+
<style>
|
|
4
|
+
path { fill: #000; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
path { fill: #FFF; }
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the public snapshot for a content artifact and bind it for the build.
|
|
3
|
+
*
|
|
4
|
+
* Writes two private files into the snapshot workspace: `snapshot.sqlite`, the
|
|
5
|
+
* finalized database, and `binding.json`, the digest-named URL the Astro build
|
|
6
|
+
* and the output copy step read. Neither is public output; only the copied,
|
|
7
|
+
* digest-named file under `dist/data/` is.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { DatabaseSync } from "../src/lib/sqlite.js";
|
|
13
|
+
import { ARTIFACT_PATH, loadArtifact } from "../src/lib/artifact-source.js";
|
|
14
|
+
import { snapshotWorkspace } from "../src/lib/snapshot-reader.js";
|
|
15
|
+
import { writeSnapshot } from "./write-snapshot.js";
|
|
16
|
+
/**
|
|
17
|
+
* Build the snapshot from the producer's in-memory entries and write the binding.
|
|
18
|
+
*
|
|
19
|
+
* The packaged build calls this directly: its serialized artifact deliberately
|
|
20
|
+
* omits `outgoing`/`backlinks`, so the edge authorities reach the snapshot
|
|
21
|
+
* builder as the transient producer result rather than through a file.
|
|
22
|
+
*/
|
|
23
|
+
export function buildSnapshotFromEntries(entries, workspace = snapshotWorkspace()) {
|
|
24
|
+
mkdirSync(workspace, { recursive: true });
|
|
25
|
+
const written = writeSnapshot({ version: 1, entries: [...entries] }, resolve(workspace, 'snapshot.sqlite'));
|
|
26
|
+
writeFileSync(resolve(workspace, 'binding.json'), `${JSON.stringify(
|
|
27
|
+
// The header constants live in code; the binding names the bytes.
|
|
28
|
+
{ url: written.url, digest: written.digest }, null, 2)}\n`, 'utf8');
|
|
29
|
+
const database = loadSnapshotCounts(written.path);
|
|
30
|
+
return { ...written, workspace, ...database };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the snapshot and write the binding beside it.
|
|
34
|
+
*
|
|
35
|
+
* @param artifactPath Repository-relative artifact to read; defaults to the one
|
|
36
|
+
* this build selected (`CONTENT_ARTIFACT` or `src/data/content.json`).
|
|
37
|
+
* @param workspace Absolute private directory. Defaults to `SNAPSHOT_WORKSPACE`
|
|
38
|
+
* or `.astro/snapshot`.
|
|
39
|
+
*/
|
|
40
|
+
export function buildSnapshot(artifactPath = ARTIFACT_PATH, workspace = snapshotWorkspace()) {
|
|
41
|
+
return buildSnapshotFromEntries(loadArtifact(artifactPath).entries, workspace);
|
|
42
|
+
}
|
|
43
|
+
/** Read the finalized file back, so the reported counts are the published rows. */
|
|
44
|
+
function loadSnapshotCounts(path) {
|
|
45
|
+
const database = new DatabaseSync(path, { readOnly: true });
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
nodes: database.prepare('SELECT count(*) AS c FROM nodes').get().c,
|
|
49
|
+
edges: database.prepare('SELECT count(*) AS c FROM edges').get().c,
|
|
50
|
+
tags: database.prepare('SELECT count(*) AS c FROM tags').get().c,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
database.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function main() {
|
|
58
|
+
try {
|
|
59
|
+
const built = buildSnapshot();
|
|
60
|
+
console.log(`snapshot ok: nodes=${built.nodes} edges=${built.edges} tags=${built.tags} bytes=${built.size} sha256=${built.digest}`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
69
|
+
process.exit(main());
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage the pinned SQLite browser runtime into the private build workspace.
|
|
3
|
+
*
|
|
4
|
+
* The package is **not** bundled by Vite: bundling it inlined its Emscripten glue
|
|
5
|
+
* into the Worker chunk, which both mixed a third-party runtime into ANC's own
|
|
6
|
+
* provenance and made the residue scan read the library's `[[` and `file://`
|
|
7
|
+
* strings as findings. Instead the package's browser entry and its WASM are
|
|
8
|
+
* copied verbatim under `/wasm/`, and the Worker imports the entry by URL at
|
|
9
|
+
* runtime (`import(/* @vite-ignore *\/ …)`), so it stays external and same-origin.
|
|
10
|
+
*
|
|
11
|
+
* The WASM is digest-named and the Worker verifies the bytes it downloads; the
|
|
12
|
+
* entry keeps its stable name because the package resolves `sqlite3.wasm`
|
|
13
|
+
* relative to itself, and `locateFile` redirects that to the hashed copy.
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { WASM_DIRECTORY, WASM_MODULE_NAME, wasmModuleUrl, wasmMemberName, wasmRoute } from "../src/lib/wasm-asset.js";
|
|
21
|
+
import { snapshotWorkspace } from "../src/lib/snapshot-reader.js";
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
/** Members copied verbatim beside the entry, keeping the package's own names. */
|
|
24
|
+
const STABLE_MEMBERS = ['sqlite3-worker1.mjs', 'sqlite3-opfs-async-proxy.js'];
|
|
25
|
+
function packageDist() {
|
|
26
|
+
const manifest = require.resolve('@sqlite.org/sqlite-wasm/package.json');
|
|
27
|
+
return join(dirname(manifest), 'dist');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve, hash, and stage the browser runtime.
|
|
31
|
+
*
|
|
32
|
+
* @param workspace Absolute private directory. Defaults to `SNAPSHOT_WORKSPACE`.
|
|
33
|
+
*/
|
|
34
|
+
export function buildWasm(workspace = snapshotWorkspace()) {
|
|
35
|
+
const dist = packageDist();
|
|
36
|
+
const wasmBytes = readFileSync(join(dist, 'sqlite3.wasm'));
|
|
37
|
+
const digest = createHash('sha256').update(wasmBytes).digest('hex');
|
|
38
|
+
const staging = resolve(workspace, WASM_DIRECTORY);
|
|
39
|
+
mkdirSync(staging, { recursive: true });
|
|
40
|
+
copyFileSync(join(dist, 'index.mjs'), resolve(staging, WASM_MODULE_NAME));
|
|
41
|
+
for (const member of STABLE_MEMBERS)
|
|
42
|
+
copyFileSync(join(dist, member), resolve(staging, member));
|
|
43
|
+
const wasmName = wasmMemberName(digest);
|
|
44
|
+
copyFileSync(join(dist, 'sqlite3.wasm'), resolve(staging, wasmName));
|
|
45
|
+
const binding = {
|
|
46
|
+
moduleUrl: wasmModuleUrl(),
|
|
47
|
+
url: wasmRoute(digest),
|
|
48
|
+
digest,
|
|
49
|
+
members: [`${WASM_DIRECTORY}/${WASM_MODULE_NAME}`, ...STABLE_MEMBERS.map((m) => `${WASM_DIRECTORY}/${m}`), `${WASM_DIRECTORY}/${wasmName}`],
|
|
50
|
+
size: wasmBytes.length,
|
|
51
|
+
};
|
|
52
|
+
writeFileSync(resolve(workspace, 'wasm.json'), `${JSON.stringify({ moduleUrl: binding.moduleUrl, url: binding.url, digest: binding.digest, members: binding.members }, null, 2)}\n`, 'utf8');
|
|
53
|
+
return binding;
|
|
54
|
+
}
|
|
55
|
+
function main() {
|
|
56
|
+
try {
|
|
57
|
+
const staged = buildWasm();
|
|
58
|
+
console.log(`wasm ok: bytes=${staged.size} sha256=${staged.digest} members=${staged.members.length}`);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
67
|
+
process.exit(main());
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Retention for packaged-build workspaces left by interruption or deferred cleanup. */
|
|
2
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const STALE_BUILD_WORKSPACE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const WORKSPACE_NAME = /^\.anc-build-[A-Za-z0-9]{6}$/;
|
|
6
|
+
/** Remove one completed workspace without replacing the build result on failure. */
|
|
7
|
+
export async function removeBuildWorkspace(directory, remove = (target) => rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })) {
|
|
8
|
+
try {
|
|
9
|
+
await remove(directory);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Best-effort removal of this tool's stale sibling workspaces.
|
|
18
|
+
*
|
|
19
|
+
* Workspaces must live beside the package so Astro's staging and output stay on
|
|
20
|
+
* one device. A killed process never reaches its `finally`, and Windows may hold
|
|
21
|
+
* a just-written file past the final retry, so a later run converges the state.
|
|
22
|
+
* The 24-hour age bound keeps concurrent workspaces out of the candidate set.
|
|
23
|
+
* Symlinks, junctions, files, and unfamiliar names are never traversed or removed.
|
|
24
|
+
*/
|
|
25
|
+
export async function pruneStaleBuildWorkspaces(root, now = Date.now()) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
let removed = 0;
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (!WORKSPACE_NAME.test(entry.name) || !entry.isDirectory())
|
|
36
|
+
continue;
|
|
37
|
+
const directory = join(root, entry.name);
|
|
38
|
+
try {
|
|
39
|
+
const metadata = await stat(directory);
|
|
40
|
+
if (now - metadata.mtimeMs <= STALE_BUILD_WORKSPACE_AGE_MS)
|
|
41
|
+
continue;
|
|
42
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
43
|
+
removed += 1;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Cleanup must never replace or prevent the build the user requested.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return removed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy the finalized snapshot into the built output under its digest name.
|
|
3
|
+
*
|
|
4
|
+
* Runs after Astro has emptied and rewritten the output directory, so a DB built
|
|
5
|
+
* before the render is not deleted by the render. The copied bytes are re-hashed
|
|
6
|
+
* against the binding: the public filename is a claim about the file, and this is
|
|
7
|
+
* where the build proves it.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { snapshotFileName } from "../src/lib/snapshot.js";
|
|
14
|
+
import { readBuildBinding, snapshotWorkspace } from "../src/lib/snapshot-reader.js";
|
|
15
|
+
/**
|
|
16
|
+
* @param outputDirectory Directory to copy into, absolute or cwd-relative.
|
|
17
|
+
* @returns The same-origin URL the copied file is bound to.
|
|
18
|
+
* @throws when no binding exists or the copied bytes do not match the digest.
|
|
19
|
+
*/
|
|
20
|
+
export function copySnapshotToOutput(outputDirectory, workspace = snapshotWorkspace()) {
|
|
21
|
+
const binding = readBuildBinding(workspace);
|
|
22
|
+
if (binding === undefined) {
|
|
23
|
+
throw new Error('no snapshot binding: run the snapshot build before copying it into the output');
|
|
24
|
+
}
|
|
25
|
+
const source = resolve(workspace, 'snapshot.sqlite');
|
|
26
|
+
const target = resolve(outputDirectory, snapshotFileName(binding.digest));
|
|
27
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
28
|
+
copyFileSync(source, target);
|
|
29
|
+
const digest = createHash('sha256').update(readFileSync(target)).digest('hex');
|
|
30
|
+
if (digest !== binding.digest) {
|
|
31
|
+
throw new Error('copied snapshot bytes do not match the digest in its bound URL');
|
|
32
|
+
}
|
|
33
|
+
return binding.url;
|
|
34
|
+
}
|
|
35
|
+
function main() {
|
|
36
|
+
const output = process.argv[2] ?? 'dist';
|
|
37
|
+
try {
|
|
38
|
+
const url = copySnapshotToOutput(output);
|
|
39
|
+
console.log(`snapshot bound: ${url}`);
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
48
|
+
process.exit(main());
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy the staged SQLite runtime into the built output under `/wasm/`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `copy-snapshot.ts`: runs after Astro has rewritten the output, copies
|
|
5
|
+
* every staged member, and re-hashes the WASM against the binding so the public
|
|
6
|
+
* filename is proven about the published bytes.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { copyFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { readWasmBinding } from "../src/lib/wasm-asset.js";
|
|
13
|
+
import { snapshotWorkspace } from "../src/lib/snapshot-reader.js";
|
|
14
|
+
/** Read the staged runtime binding, or `undefined` when this build staged none. */
|
|
15
|
+
export function readStagedWasm(workspace = snapshotWorkspace()) {
|
|
16
|
+
try {
|
|
17
|
+
return readWasmBinding(JSON.parse(readFileSync(resolve(workspace, 'wasm.json'), 'utf8')));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @returns The same-origin URL the copied WASM is bound to.
|
|
25
|
+
* @throws when no binding exists or the copied bytes do not match the digest.
|
|
26
|
+
*/
|
|
27
|
+
export function copyWasmToOutput(outputDirectory, workspace = snapshotWorkspace()) {
|
|
28
|
+
const binding = readStagedWasm(workspace);
|
|
29
|
+
if (binding === undefined) {
|
|
30
|
+
throw new Error('no wasm binding: run the wasm build before copying it into the output');
|
|
31
|
+
}
|
|
32
|
+
const staging = resolve(workspace, 'wasm');
|
|
33
|
+
for (const member of binding.members) {
|
|
34
|
+
const name = member.split('/').at(-1);
|
|
35
|
+
const target = resolve(outputDirectory, member);
|
|
36
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
37
|
+
copyFileSync(resolve(staging, name), target);
|
|
38
|
+
}
|
|
39
|
+
const digest = createHash('sha256').update(readFileSync(resolve(outputDirectory, binding.url.slice(1)))).digest('hex');
|
|
40
|
+
if (digest !== binding.digest) {
|
|
41
|
+
throw new Error('copied wasm bytes do not match the digest in their bound URL');
|
|
42
|
+
}
|
|
43
|
+
return binding.url;
|
|
44
|
+
}
|
|
45
|
+
function main() {
|
|
46
|
+
const output = process.argv[2] ?? 'dist';
|
|
47
|
+
try {
|
|
48
|
+
console.log(`wasm bound: ${copyWasmToOutput(output)}`);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
57
|
+
process.exit(main());
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits `dist/_redirects` after the Astro build.
|
|
3
|
+
*
|
|
4
|
+
* Generated rather than committed under `public/`, so the redirect map cannot
|
|
5
|
+
* drift from the artifact it describes.
|
|
6
|
+
*
|
|
7
|
+
* The map is empty today, and correctly so: no public URL has ever been
|
|
8
|
+
* stranded (see {@link renderRedirects}). What ships is the version-stamped
|
|
9
|
+
* header, which is what ties a deployed map back to the artifact that produced
|
|
10
|
+
* it — and the working, tested emission path for the first rename that does
|
|
11
|
+
* strand one.
|
|
12
|
+
*
|
|
13
|
+
* A build step rather than an Astro route: `src/pages/_redirects.ts` would be
|
|
14
|
+
* ignored (Astro excludes underscore-prefixed files from routing), and the file
|
|
15
|
+
* is consumed by the host rather than served, so it does not belong in the
|
|
16
|
+
* route model. It follows `scripts/validate-content.ts` — same shape, same
|
|
17
|
+
* place in the build chain.
|
|
18
|
+
*/
|
|
19
|
+
import { writeFileSync } from 'node:fs';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { SCHEMA_VERSION } from "../src/lib/schema.js";
|
|
23
|
+
import { REDIRECT_RULES, renderRedirects } from "../src/lib/routes.js";
|
|
24
|
+
import { loadArtifact, readArtifact } from "../src/lib/artifact-source.js";
|
|
25
|
+
import { contentVersion } from "./validate-content.js";
|
|
26
|
+
const DIST = fileURLToPath(new URL('../dist', import.meta.url));
|
|
27
|
+
/**
|
|
28
|
+
* Write `_redirects` into a built site.
|
|
29
|
+
*
|
|
30
|
+
* @param outDirectory Where the build actually wrote. Defaults to this
|
|
31
|
+
* repository's own `dist/`, which is where `pnpm run build` puts it. The
|
|
32
|
+
* packaged CLI builds into a staging directory instead — Astro renames assets
|
|
33
|
+
* out of `<root>/.astro` and a rename cannot cross a device — so the emitter
|
|
34
|
+
* must be told, rather than assuming the one path that used to be the only
|
|
35
|
+
* possibility.
|
|
36
|
+
*/
|
|
37
|
+
export function emitRedirects(outDirectory = DIST) {
|
|
38
|
+
// Re-validated rather than imported through `src/lib/content.ts`: the
|
|
39
|
+
// accessor is written for the Astro build, and reading the artifact here
|
|
40
|
+
// keeps this script runnable on its own. It reads whichever artifact the
|
|
41
|
+
// build read, so a fixture build's redirect map describes the fixture.
|
|
42
|
+
//
|
|
43
|
+
// The validated result is discarded: the rule set is a literal, so nothing
|
|
44
|
+
// emitted here is derived from the entries. The call is the validation.
|
|
45
|
+
loadArtifact();
|
|
46
|
+
writeFileSync(join(outDirectory, '_redirects'), renderRedirects(REDIRECT_RULES, {
|
|
47
|
+
schema: SCHEMA_VERSION,
|
|
48
|
+
content: contentVersion(readArtifact()),
|
|
49
|
+
}), 'utf8');
|
|
50
|
+
return REDIRECT_RULES.length;
|
|
51
|
+
}
|
|
52
|
+
function main() {
|
|
53
|
+
try {
|
|
54
|
+
const rules = emitRedirects();
|
|
55
|
+
console.log(`redirects ok: ${rules} permanent rule${rules === 1 ? '' : 's'}`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (process.argv[1] === fileURLToPath(import.meta.url))
|
|
64
|
+
process.exit(main());
|