@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ANC contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# anc — a privacy-preserving static site generator for Markdown notes and digital gardens
|
|
2
|
+
|
|
3
|
+
[](#status)
|
|
4
|
+
[](#working-on-the-tool)
|
|
5
|
+
[](https://github.com/wxxb789/anc/actions/workflows/verify.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](docs/core-design/README.md)
|
|
8
|
+
|
|
9
|
+
[English](README.md) · [简体中文](README_zh-cn.md)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
**anc turns a git repository of Markdown into a self-hosted static knowledge
|
|
14
|
+
garden**: articles, backlinks, a link graph, breadcrumbs, tags, collections, a table
|
|
15
|
+
of contents, full-text search, feeds, and a sitemap — all rendered ahead of time into
|
|
16
|
+
plain static files that any host can serve.
|
|
17
|
+
|
|
18
|
+
It reads the notes you already have, including Obsidian-style `[[wikilinks]]`, and
|
|
19
|
+
needs no database, server, or account. Every note publishes unless you exclude it.
|
|
20
|
+
Ordinary reading needs no JavaScript, and nothing is sent to a third party.
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
*anc* is short for **A**ctive **N**oise **C**ancelling. The command it installs is
|
|
25
|
+
`anc`; the package is named `@wxxb789/anc` and is not yet published to npm.
|
|
26
|
+
|
|
27
|
+
## Contents
|
|
28
|
+
|
|
29
|
+
- [Status](#status)
|
|
30
|
+
- [Why anc](#why-anc)
|
|
31
|
+
- [Features](#features)
|
|
32
|
+
- [How it works](#how-it-works)
|
|
33
|
+
- [Screenshots](#screenshots)
|
|
34
|
+
- [Quick start](#quick-start)
|
|
35
|
+
- [How publication is decided](#how-publication-is-decided)
|
|
36
|
+
- [Links, tags, and metadata](#links-tags-and-metadata)
|
|
37
|
+
- [Architecture](#architecture)
|
|
38
|
+
- [Working on the tool](#working-on-the-tool)
|
|
39
|
+
- [Adoption and hosting](#adoption-and-hosting)
|
|
40
|
+
- [FAQ](#faq)
|
|
41
|
+
- [Documentation](#documentation)
|
|
42
|
+
- [License](#license)
|
|
43
|
+
|
|
44
|
+
## Status
|
|
45
|
+
|
|
46
|
+
**Pre-release.** The tool builds, previews, and ships the GitHub Action and the `init`
|
|
47
|
+
command. It is named `@wxxb789/anc`, because the unscoped `anc` is already taken on
|
|
48
|
+
npm, and it is not on the registry yet; install from a checkout or from the tarball the
|
|
49
|
+
repository builds. [`docs/adoption.md`](docs/adoption.md) gives both, along with
|
|
50
|
+
configuration, exclusion, links, and hosting. ANC is not yet 0.1.0-ready or
|
|
51
|
+
1.0.0-ready and makes no backward-compatibility promise.
|
|
52
|
+
|
|
53
|
+
## Why anc
|
|
54
|
+
|
|
55
|
+
- **Your notes stay yours.** Markdown in git is the source of truth. There is no
|
|
56
|
+
import step, no proprietary vault format, and no hosted service to subscribe to.
|
|
57
|
+
- **Privacy is decided at publication.** `publish: false` and exclusion globs withhold
|
|
58
|
+
a note, a typo in a pattern fails the build instead of leaking drafts, and the build
|
|
59
|
+
log never prints a withheld file's name.
|
|
60
|
+
- **A digital garden, not a blog.** Backlinks and the link graph are computed at
|
|
61
|
+
build time from how your notes link; hover previews and graph exploration read the
|
|
62
|
+
same public snapshot lazily.
|
|
63
|
+
- **Static and portable.** The output is a `dist/` folder of HTML, CSS, a Pagefind
|
|
64
|
+
index, and one SQLite file. GitHub Pages, Cloudflare Pages, Netlify, or any web
|
|
65
|
+
server can host it at the root of a domain.
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
| Surface | What you get |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| Markdown | CommonMark plus GFM: tables, task lists, footnotes, callouts, highlighted code |
|
|
72
|
+
| Links | `[[wikilinks]]`, relative and root-anchored links, Markdown links, and note embeds |
|
|
73
|
+
| Relationships | Backlinks, outgoing links, and a link graph, rendered to static HTML |
|
|
74
|
+
| Navigation | Tags, collections, breadcrumbs, table of contents, and recent changes |
|
|
75
|
+
| Search | Pagefind full-text search, with a per-document language |
|
|
76
|
+
| Rich content | Client-rendered math and Mermaid diagrams, each with a source fallback |
|
|
77
|
+
| Discovery | Atom feed, sitemap, `robots.txt`, canonical URLs, and Open Graph metadata |
|
|
78
|
+
| Languages | Per-document chrome in English and Simplified Chinese (`zh-CN`) |
|
|
79
|
+
| Privacy | Default-publish with explicit withholding; released names stay out of logs |
|
|
80
|
+
| Delivery | Static HTML by default; a public SQLite/WASM snapshot for lazy previews |
|
|
81
|
+
| Security | A strict Content-Security-Policy in `dist/_headers`; no trackers or analytics |
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
1. **Write** Markdown in the git repository you already keep, in any editor —
|
|
88
|
+
Obsidian, VS Code, or plain `vim`.
|
|
89
|
+
2. **Build** with `anc build`. Exclusions are applied first, then every link is
|
|
90
|
+
resolved and every page is rendered ahead of time.
|
|
91
|
+
3. **Host** the resulting `dist/` folder anywhere that serves static files.
|
|
92
|
+
|
|
93
|
+
## Screenshots
|
|
94
|
+
|
|
95
|
+
Every image below is a real page built by anc from a small synthetic garden;
|
|
96
|
+
[`docs/assets/make-screenshots.mjs`](docs/assets/make-screenshots.mjs) regenerates them.
|
|
97
|
+
|
|
98
|
+
| Backlinks and outgoing links | Local link graph |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
|  |  |
|
|
101
|
+
| **Hover preview** | **Full-text search** |
|
|
102
|
+
|  |  |
|
|
103
|
+
|
|
104
|
+
## Quick start
|
|
105
|
+
|
|
106
|
+
From a checkout of this repository, against your own notes:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
node /path/to/anc/bin/anc.mjs build --content ~/notes --out ~/notes/dist
|
|
110
|
+
node /path/to/anc/bin/anc.mjs preview --dist ~/notes/dist
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or install the tarball the repository builds:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd /path/to/anc && pnpm run pack:tarball
|
|
117
|
+
cd ~/notes
|
|
118
|
+
pnpm add -D /path/to/anc/wxxb789-anc-*.tgz # or: npm install -D /path/to/anc/wxxb789-anc-*.tgz
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then name the commands in your notes repository's `package.json`; the same scripts
|
|
122
|
+
work under pnpm and npm:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build": "anc build",
|
|
128
|
+
"preview": "anc preview",
|
|
129
|
+
"review": "anc review",
|
|
130
|
+
"release": "anc build --release"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pnpm run build # or: npm run build
|
|
137
|
+
pnpm run preview # serves dist/ at http://localhost:4321/
|
|
138
|
+
pnpm run review # write .publish-set.json for inspection
|
|
139
|
+
pnpm run release # exact publish set + pinned Gitleaks on PATH
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To call the binary directly, use `pnpm exec anc build`, or `npx --no anc build` under
|
|
143
|
+
npm: `--no` makes `npx` refuse rather than fetch from the registry when the local
|
|
144
|
+
install is missing. After publication the registry form will be
|
|
145
|
+
`npx @wxxb789/anc build`. **Do not type bare `npx anc`:** outside a project with the
|
|
146
|
+
tarball installed it falls back to the registry, where the unscoped `anc` is an
|
|
147
|
+
unrelated third-party package that `npx` would offer to download and run.
|
|
148
|
+
|
|
149
|
+
## How publication is decided
|
|
150
|
+
|
|
151
|
+
**Everything publishes unless you exclude it.** There is no allowlist and no
|
|
152
|
+
`publish: true` to opt in with. Two mechanisms withhold a note, and when they
|
|
153
|
+
disagree the one pointing at *not publishing* wins:
|
|
154
|
+
|
|
155
|
+

|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
---
|
|
159
|
+
publish: false
|
|
160
|
+
---
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
# publish.config.yaml
|
|
165
|
+
title: Field Notes
|
|
166
|
+
origin: https://notes.example.org/
|
|
167
|
+
exclude:
|
|
168
|
+
- "drafts/**"
|
|
169
|
+
- "clients/**"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A pattern that matches nothing stops the build. That is deliberate: `draft/**` typed
|
|
173
|
+
for `drafts/**` would otherwise build green while the drafts went live. A link to a
|
|
174
|
+
withheld note keeps the full label and path you wrote, resolves to `/private/`, and is
|
|
175
|
+
reported; the withheld note's own body, title, and excerpt reach nothing.
|
|
176
|
+
|
|
177
|
+
The build prints counts and no filenames. The list of dropped files goes to
|
|
178
|
+
`content-report.json` under `<git-dir>/publish-report/` (or your user state directory
|
|
179
|
+
outside git), which `git add -A` cannot reach and which is never copied into `dist/`.
|
|
180
|
+
A workflow log on a public repository is world-readable, so a withheld file's path is
|
|
181
|
+
a disclosure there; that is why the names and the counts are split.
|
|
182
|
+
|
|
183
|
+
## Links, tags, and metadata
|
|
184
|
+
|
|
185
|
+
- Five link spellings resolve in one pass, following Obsidian's own order: `[[note]]`,
|
|
186
|
+
`[[./sibling]]`, `[[folder/note]]`, `[text](../other.md)`, and `[[note|shown]]`.
|
|
187
|
+
- Tags come from a frontmatter list, and each one gets a `/tags/<key>/` page. The
|
|
188
|
+
first folder under the content root becomes the flat collection.
|
|
189
|
+
- Git history supplies `created` and `updated`; frontmatter does not.
|
|
190
|
+
- Frontmatter can set `slug`, `language`, `description`, `tags`, and `aliases`.
|
|
191
|
+
Aliases are public, searchable metadata with no route of their own — deliberately
|
|
192
|
+
not alternate link targets.
|
|
193
|
+
- Non-Markdown files are never published. There is no asset pipeline, so an embedded
|
|
194
|
+
image degrades to text and is reported.
|
|
195
|
+
|
|
196
|
+
## Architecture
|
|
197
|
+
|
|
198
|
+
Long-term architecture is documented in [`docs/core-design/`](docs/core-design/README.md);
|
|
199
|
+
bounded development outcomes and their completion evidence live in
|
|
200
|
+
[`docs/goals/`](docs/goals/README.md). Markdown stays canonical, the compiler IR stays
|
|
201
|
+
private, static HTML delivers pages, and Pagefind owns full-text search.
|
|
202
|
+
|
|
203
|
+
- Astro `output: "static"`; the build output is `dist/`.
|
|
204
|
+
- Content is produced by `scripts/markdown-to-artifact.ts` and `scripts/resolve-links.ts`,
|
|
205
|
+
then validated against `src/lib/schema.ts` before anything renders.
|
|
206
|
+
- The one public relational and preview index is `data/site.<sha256>.sqlite`, holding
|
|
207
|
+
`nodes`, `edges`, `aliases`, `tags`, and `node_tags`. It contains no page body and no
|
|
208
|
+
SQLite FTS; the browser fetches it lazily in a read-only Worker for hover previews,
|
|
209
|
+
tag browsing, and graph exploration.
|
|
210
|
+
- Base interactive surfaces are a few KB gzip of vanilla script. Pages containing math
|
|
211
|
+
or diagrams lazy-load the accepted client renderers; both have source fallbacks.
|
|
212
|
+
- No D1, R2, Functions, analytics, comments, or build-time data service. A runtime
|
|
213
|
+
application server is not required.
|
|
214
|
+
|
|
215
|
+
## Working on the tool
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pnpm install
|
|
219
|
+
pnpm run verify # lint, check, build, inventory, secret/residue scans, tests
|
|
220
|
+
pnpm run build # the build chain alone
|
|
221
|
+
pnpm run build:fixture # rebuild against the 32-note corpus
|
|
222
|
+
pnpm run build:example # build example/ into .tmp/example-dist
|
|
223
|
+
pnpm run preview:example # serve the example build locally
|
|
224
|
+
pnpm run pack:tarball # compile TypeScript and pack the installable tarball
|
|
225
|
+
pnpm run smoke:tarball # install that tarball in a foreign repo and read it
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`pnpm run verify` requires the exact Gitleaks version exported by
|
|
229
|
+
`scripts/scan-secrets.ts` on `PATH`; an ordinary `pnpm run build` does not.
|
|
230
|
+
`packageManager` in `package.json` pins the pnpm version, which `corepack enable`
|
|
231
|
+
honours. Dependencies install into a symlinked `node_modules`, so a package not
|
|
232
|
+
declared in `package.json` does not resolve — a boundary rather than a preference.
|
|
233
|
+
|
|
234
|
+
Read [`AGENTS.md`](AGENTS.md) before changing anything. It carries the verification
|
|
235
|
+
contract, what runs where, and what is known stale and whose it is. The synthetic
|
|
236
|
+
corpus under [`example/`](example/) is the smallest useful feature tour, with public
|
|
237
|
+
notes, both exclusion mechanisms, links and backlinks, rich Markdown, math, and Mermaid.
|
|
238
|
+
|
|
239
|
+
## Adoption and hosting
|
|
240
|
+
|
|
241
|
+
The GitHub Action at the repository root runs a release build on a supported Linux
|
|
242
|
+
runner, installs the checksum-pinned secret scanner, and writes a static `dist/`. Any
|
|
243
|
+
static host serves that directory, at the root of a domain: a site under a path such
|
|
244
|
+
as `https://<user>.github.io/<repo>/` is not supported. `dist/_headers` carries a
|
|
245
|
+
Content-Security-Policy and three other security headers in Cloudflare Pages' format;
|
|
246
|
+
a host that ignores that file still gets the policy from each page's `<meta>` tag,
|
|
247
|
+
except `frame-ancestors`, and serves the other headers only if configured to.
|
|
248
|
+
|
|
249
|
+
Publication is an explicit external side effect. Building does not deploy, and nothing
|
|
250
|
+
in this repository can. [`docs/adoption.md`](docs/adoption.md) covers both the Action
|
|
251
|
+
and a hand-assembled GitHub Pages example, with no secrets required.
|
|
252
|
+
|
|
253
|
+
## FAQ
|
|
254
|
+
|
|
255
|
+
**Can anc publish an Obsidian vault?**
|
|
256
|
+
Yes: point `--content` at the vault. anc reads Obsidian-style `[[wikilinks]]`,
|
|
257
|
+
`[[note|shown text]]`, note embeds, and callouts; keeping the vault in git also gives
|
|
258
|
+
each page its created and updated dates. Plugins and non-Markdown attachments are not
|
|
259
|
+
processed.
|
|
260
|
+
|
|
261
|
+
**Do I need a server, a database, or an account?**
|
|
262
|
+
No. The output is static files. Search runs from a Pagefind index and relationships
|
|
263
|
+
from one SQLite file, both fetched by the reader's browser only when used.
|
|
264
|
+
|
|
265
|
+
**How do I keep a note private?**
|
|
266
|
+
Add `publish: false` to its frontmatter, or match its path with an `exclude:` glob in
|
|
267
|
+
`publish.config.yaml`. See [How publication is decided](#how-publication-is-decided).
|
|
268
|
+
|
|
269
|
+
**Does the site work without JavaScript?**
|
|
270
|
+
Reading, navigation, backlinks, and the link graph's table fallback are plain HTML.
|
|
271
|
+
Search, hover previews, math, and diagrams need JavaScript.
|
|
272
|
+
|
|
273
|
+
**Where can I host it?**
|
|
274
|
+
Any static host at the root of a domain: GitHub Pages, Cloudflare Pages, Netlify, or
|
|
275
|
+
your own web server. See [Adoption and hosting](#adoption-and-hosting).
|
|
276
|
+
|
|
277
|
+
## Documentation
|
|
278
|
+
|
|
279
|
+
- [`docs/adoption.md`](docs/adoption.md) — what a stranger with a notes repository does.
|
|
280
|
+
- [`docs/core-design/`](docs/core-design/README.md) — authoritative long-term architecture.
|
|
281
|
+
- [`docs/goals/`](docs/goals/README.md) — active goals and the completed-goal archive.
|
|
282
|
+
- [`docs/gate-reading.md`](docs/gate-reading.md) — the seven ways an instrument lies
|
|
283
|
+
about itself.
|
|
284
|
+
- [`AGENTS.md`](AGENTS.md) — contributor workflow and the verification contract.
|
|
285
|
+
- [`example/`](example/) — a synthetic corpus demonstrating every public surface.
|
|
286
|
+
|
|
287
|
+
[Astro documentation](https://docs.astro.build) · [Pagefind documentation](https://pagefind.app/docs/)
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
Released under the [MIT License](LICENSE).
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
[English](README.md) · [简体中文](README_zh-cn.md)
|
package/astro.config.mjs
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { defineConfig } from 'astro/config';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { wasmModuleUrl } from './src/lib/wasm-asset.js';
|
|
5
|
+
import { configuredSnapshotWorkspace } from './src/lib/snapshot.js';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { DIAGRAM_MODE } from './src/lib/diagram-mode.js';
|
|
9
|
+
import { MATH_MODE } from './src/lib/math-mode.js';
|
|
10
|
+
import { configForBuild } from './scripts/load-config.js';
|
|
11
|
+
import { vendorProvenancePlugin } from './scripts/vendor-provenance.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The private snapshot workspace this build staged into.
|
|
15
|
+
*
|
|
16
|
+
* The empty-as-unset choice lives in `configuredSnapshotWorkspace`, shared with
|
|
17
|
+
* `snapshot-reader.ts`; the `??` this line used to carry resolved an empty
|
|
18
|
+
* value to the working directory while the reader fell back to the default, so
|
|
19
|
+
* the binding below was substituted as `null` and the Worker silently never
|
|
20
|
+
* started.
|
|
21
|
+
*/
|
|
22
|
+
const SNAPSHOT_WORKSPACE = resolve(process.cwd(), configuredSnapshotWorkspace(process.env['SNAPSHOT_WORKSPACE']));
|
|
23
|
+
|
|
24
|
+
/** Read once: the binding and its module URL come from the same file. */
|
|
25
|
+
const STAGED_WASM = readStaged('wasm.json');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Read one staged binding from the private snapshot workspace.
|
|
29
|
+
*
|
|
30
|
+
* The design calls for a private generated binding rather than a public manifest
|
|
31
|
+
* (`docs/core-design/build-and-runtime.md`). `vite.define` substitutes the
|
|
32
|
+
* digest-named same-origin URLs into the hashed client/worker bundles at build
|
|
33
|
+
* time: it carries no local path and no corpus data, and a build with no staging
|
|
34
|
+
* (dev, unit tests) substitutes `null`, so the Worker fails closed and fetches
|
|
35
|
+
* nothing. A virtual module was tried first and rejected: Vite's worker
|
|
36
|
+
* environment did not run the plugin's resolver for it.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} name
|
|
39
|
+
* @returns {Record<string, any> | null}
|
|
40
|
+
*/
|
|
41
|
+
function readStaged(name) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(resolve(SNAPSHOT_WORKSPACE, name), 'utf8'));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Keep a client runtime out of the build unless client mode wants it.
|
|
51
|
+
*
|
|
52
|
+
* `src/pages/notes/[slug].astro` has Astro `<script>` tags importing
|
|
53
|
+
* `src/scripts/diagram.ts` and `src/scripts/math.ts`, and Astro bundles such a
|
|
54
|
+
* script **eagerly**: wrapping the tag in a condition or guarding the import at
|
|
55
|
+
* runtime does not keep it out of `dist/`. Measured, not assumed — the guarded
|
|
56
|
+
* version emitted 99 Mermaid chunks on a build whose mode was `build-time`,
|
|
57
|
+
* orphaned and referenced by no page, and tripped the privacy residue scan on
|
|
58
|
+
* `[[` byte sequences inside the parser.
|
|
59
|
+
*
|
|
60
|
+
* A plugin rather than `resolve.alias`, because an alias is matched against the
|
|
61
|
+
* *raw specifier* — here `../../scripts/diagram.ts` — before resolution, so a
|
|
62
|
+
* pattern written against the real path never fires. `resolveId` runs after the
|
|
63
|
+
* path is known, which is the only place the question "is this that module" can
|
|
64
|
+
* be asked reliably.
|
|
65
|
+
*
|
|
66
|
+
* Typed structurally rather than as `import('vite').Plugin`: `vite` reaches this
|
|
67
|
+
* tree through Astro and is not a declared dependency, and under pnpm an
|
|
68
|
+
* undeclared package does not resolve — which is the boundary `AGENTS.md` names
|
|
69
|
+
* as a feature. Adding `vite` to `package.json` to annotate one function would
|
|
70
|
+
* spend a dependency on a type.
|
|
71
|
+
*
|
|
72
|
+
* @param {'diagram' | 'math'} runtime
|
|
73
|
+
* @param {'build-time' | 'client'} mode
|
|
74
|
+
* @returns {{
|
|
75
|
+
* name: string,
|
|
76
|
+
* enforce: 'pre',
|
|
77
|
+
* resolveId: (
|
|
78
|
+
* this: { resolve: (source: string, importer: string | undefined, options: object) =>
|
|
79
|
+
* Promise<{ id: string } | null> },
|
|
80
|
+
* source: string,
|
|
81
|
+
* importer: string | undefined,
|
|
82
|
+
* options: object,
|
|
83
|
+
* ) => Promise<string | null>,
|
|
84
|
+
* }}
|
|
85
|
+
*/
|
|
86
|
+
function clientRuntimePlugin(runtime, mode) {
|
|
87
|
+
const stub = fileURLToPath(new URL(`src/scripts/${runtime}-disabled.ts`, import.meta.url));
|
|
88
|
+
// The extension is matched loosely because it is not the same in both trees:
|
|
89
|
+
// this repository runs `diagram.ts` directly, while the packaged tarball ships
|
|
90
|
+
// it compiled to `diagram.js` (`scripts/compile-package.ts`). Pinning `.ts`
|
|
91
|
+
// here meant the redirect silently stopped firing once installed — Mermaid's
|
|
92
|
+
// whole runtime was then bundled into a `build-time` site, which the residue
|
|
93
|
+
// scan caught on `[[` byte sequences inside the parser. Matching either
|
|
94
|
+
// extension keeps one rule true of both trees.
|
|
95
|
+
//
|
|
96
|
+
// **Parameterised over the runtime rather than duplicated**, because math
|
|
97
|
+
// needs exactly this arrangement for exactly this reason and a second copy is
|
|
98
|
+
// a second place for the `.ts`/`.js` lesson above to be relearned. The two
|
|
99
|
+
// modes are separate constants (`DIAGRAM_MODE`, `MATH_MODE`) — the costs and
|
|
100
|
+
// the evidence differ, so one switch must not carry the other's consequences —
|
|
101
|
+
// but the mechanism that keeps an unselected runtime out of `dist/` is one.
|
|
102
|
+
const target = new RegExp(String.raw`[\\/]src[\\/]scripts[\\/]${runtime}\.(?:ts|js)$`);
|
|
103
|
+
return {
|
|
104
|
+
name: `anc:${runtime}-mode`,
|
|
105
|
+
// Ahead of Vite's own resolver, so the redirect happens before the module
|
|
106
|
+
// is loaded and its imports crawled.
|
|
107
|
+
enforce: 'pre',
|
|
108
|
+
async resolveId(source, importer, options) {
|
|
109
|
+
if (mode === 'client') return null;
|
|
110
|
+
const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
|
|
111
|
+
return resolved !== null && target.test(resolved.id) ? stub : null;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* THE ONE PLACE A SITE'S PUBLIC ORIGIN IS WRITTEN DOWN.
|
|
118
|
+
*
|
|
119
|
+
* **`.localhost` rather than `.invalid`, and the change is the point of
|
|
120
|
+
* TK-31.** RFC 6761 reserves `.localhost` and requires resolvers to map the
|
|
121
|
+
* name *and all its subdomains* to loopback, so this is a name that cannot
|
|
122
|
+
* resolve to anybody else's server — the property `.invalid` was chosen for —
|
|
123
|
+
* and that additionally *works*: a preview build's canonical links, feed, and
|
|
124
|
+
* sitemap all point at a local origin a browser can actually follow.
|
|
125
|
+
*
|
|
126
|
+
* The value it replaced was this project's own name under `.invalid`, which was safe and
|
|
127
|
+
* was still one owner's name on every stranger's site. That is the whole of
|
|
128
|
+
* plan decision D2. `.invalid` was never wrong about *safety* — TK-24's gate
|
|
129
|
+
* was right that a reserved name cannot misdirect a crawler — it was wrong
|
|
130
|
+
* about *whose*, and a default that belongs to nobody is what this ticket owes
|
|
131
|
+
* every user who configures nothing.
|
|
132
|
+
*
|
|
133
|
+
* **Deliberately no port**, per plan §5.5: a canonical URL, a feed id, and a
|
|
134
|
+
* `<loc>` should not carry a development port, and they are not deployable
|
|
135
|
+
* anyway. `astro dev` serves `http://localhost:4321/` regardless; what this
|
|
136
|
+
* governs is the absolute URLs written *into* the built documents.
|
|
137
|
+
*
|
|
138
|
+
* **This is a preview origin, not a publishable one, and nothing here can tell
|
|
139
|
+
* the difference.** A site built with it is complete and correct and obviously
|
|
140
|
+
* local at a glance — which is a property `.invalid` gave up by producing URLs
|
|
141
|
+
* that looked plausible. Refusing to *deploy* it belongs to the deploy step,
|
|
142
|
+
* which this repository does not have and does not perform.
|
|
143
|
+
*
|
|
144
|
+
* **A user's own `publish.config.yaml` overrides it, and this stays the
|
|
145
|
+
* fallback rather than moving into the loader.** That is the point of the
|
|
146
|
+
* arrangement rather than an accident of it: a build of *this* repository, and
|
|
147
|
+
* a stranger's build before they have configured anything, both need an origin,
|
|
148
|
+
* and there is exactly one place to read it from. `scripts/load-config.ts`
|
|
149
|
+
* deliberately returns `undefined` for an unconfigured origin — a default there
|
|
150
|
+
* would be a second home for this value. `tests/metadata.test.ts` fails if the
|
|
151
|
+
* host appears in any file under `src/`, `scripts/`, `tests/`, or `public/`
|
|
152
|
+
* other than this one.
|
|
153
|
+
*/
|
|
154
|
+
const DEFAULT_ORIGIN = 'http://publish.localhost/';
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The user's configuration, read once at config evaluation.
|
|
158
|
+
*
|
|
159
|
+
* `configForBuild` resolves the directory from `PUBLISH_CONFIG_DIR`, falling
|
|
160
|
+
* back to the working directory. A packaged build has already changed its
|
|
161
|
+
* working directory to the installed package by the time this runs, which is
|
|
162
|
+
* why the variable exists at all; `bin/anc.mjs` documents the
|
|
163
|
+
* same boundary for `CONTENT_ARTIFACT`. For a build of *this* repository the
|
|
164
|
+
* fallback is the repository root — so a `publish.config.yaml` committed here
|
|
165
|
+
* would reconfigure this repository's own build, which is the honest
|
|
166
|
+
* description and not a bug: this repository is the tool, and there is no such
|
|
167
|
+
* file in it.
|
|
168
|
+
*
|
|
169
|
+
* A malformed config stops the build before a single page is generated — the
|
|
170
|
+
* ordering `docs/plans/ssg-generalisation-plan.md` §4.3 asks for, where a config
|
|
171
|
+
* error cannot be masked by a build that otherwise succeeded.
|
|
172
|
+
*
|
|
173
|
+
* **Caught rather than thrown, because this module scope has no boundary above
|
|
174
|
+
* it that knows how to print one.** `bin/anc.mjs` prints a
|
|
175
|
+
* `BuildFailure`'s own message and nothing else; a `astro build` or `astro dev`
|
|
176
|
+
* run directly has only Astro's config loader, which was measured printing the
|
|
177
|
+
* composed message *plus* a stack trace carrying four absolute host paths —
|
|
178
|
+
* `at refuse (Q:/…/scripts/load-config.ts:313:8)` and three more. TK-25 §2.3
|
|
179
|
+
* forbids a discovered filesystem path on that stream, and `astro dev`
|
|
180
|
+
* evaluates this same scope, so the leak is not confined to the packaged path.
|
|
181
|
+
* Printing the message and exiting keeps the diagnostic and drops the trace.
|
|
182
|
+
*/
|
|
183
|
+
function loadUserConfig() {
|
|
184
|
+
try {
|
|
185
|
+
return configForBuild();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Only a message composed under the disclosure rule may be printed. Anything
|
|
188
|
+
// else is a string this project did not write, on a world-readable stream.
|
|
189
|
+
if (error instanceof Error && error.name === 'BuildFailure') {
|
|
190
|
+
console.error(error.message);
|
|
191
|
+
} else {
|
|
192
|
+
console.error('the configuration file could not be read');
|
|
193
|
+
}
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const config = loadUserConfig();
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Hand the configured title to the page modules.
|
|
202
|
+
*
|
|
203
|
+
* `src/lib/site.ts` reads it from here and cannot read it from anywhere else:
|
|
204
|
+
* importing `scripts/load-config.ts` from `src/lib/` breaks the build, because
|
|
205
|
+
* Astro evaluates that module out of `dist/.prerender/` and the loader's
|
|
206
|
+
* dependency reads `../package.json` relative to its own URL — measured,
|
|
207
|
+
* `ENOENT: … dist\.prerender\package.json` at "generating static routes". The
|
|
208
|
+
* variable is the same seam `CONTENT_ARTIFACT` already uses across the same
|
|
209
|
+
* boundary, and that module documents the mechanism at length.
|
|
210
|
+
*
|
|
211
|
+
* Set here rather than in `bin/anc.mjs` because this is the
|
|
212
|
+
* one scope that runs for *every* way a build starts — the packaged binary,
|
|
213
|
+
* `astro build` in a checkout, and `astro dev` — and each of them needs the
|
|
214
|
+
* title. `config.title` always holds a value: `loadConfig` applies its own
|
|
215
|
+
* default when the user configured nothing, which is why there is no `??` here
|
|
216
|
+
* and why an unconfigured build still gets a legible name.
|
|
217
|
+
*
|
|
218
|
+
* **The name is spelled out rather than imported, deliberately.** Importing
|
|
219
|
+
* `SITE_TITLE_VARIABLE` from `src/lib/site.ts` would evaluate that module here,
|
|
220
|
+
* at config time, *before* this line runs — so its `SITE_NAME` would be
|
|
221
|
+
* computed from an unset variable, giving one process two module instances that
|
|
222
|
+
* disagree about the site's name. A literal has no such ordering. The cost is a
|
|
223
|
+
* second copy of the spelling, and it is paid the way this repository already
|
|
224
|
+
* pays it for `theme-init.js`: `tests/site-identity.test.ts` reads both files
|
|
225
|
+
* and fails if they name different variables, so a rename cannot half-apply.
|
|
226
|
+
*/
|
|
227
|
+
process.env['PUBLISH_SITE_TITLE'] = config.title;
|
|
228
|
+
|
|
229
|
+
// https://astro.build/config
|
|
230
|
+
export default defineConfig({
|
|
231
|
+
site: config.origin ?? DEFAULT_ORIGIN,
|
|
232
|
+
output: 'static',
|
|
233
|
+
trailingSlash: 'always',
|
|
234
|
+
vite: {
|
|
235
|
+
define: {
|
|
236
|
+
__ANC_SNAPSHOT_BINDING__: JSON.stringify(readStaged('binding.json')),
|
|
237
|
+
__ANC_WASM_BINDING__: JSON.stringify(STAGED_WASM),
|
|
238
|
+
__ANC_WASM_MODULE_URL__: JSON.stringify(STAGED_WASM?.['moduleUrl'] ?? wasmModuleUrl()),
|
|
239
|
+
},
|
|
240
|
+
plugins: [
|
|
241
|
+
clientRuntimePlugin('diagram', DIAGRAM_MODE),
|
|
242
|
+
clientRuntimePlugin('math', MATH_MODE),
|
|
243
|
+
vendorProvenancePlugin(fileURLToPath(new URL('.', import.meta.url))),
|
|
244
|
+
],
|
|
245
|
+
build: {
|
|
246
|
+
// Load-bearing for the CSP, not a size preference. Above 0, Vite inlines
|
|
247
|
+
// a small `?url` asset as a `data:` URL — and `src/scripts/theme-init.js`
|
|
248
|
+
// is exactly that, imported by `Layout.astro`. A `data:` script violates
|
|
249
|
+
// `script-src 'self'`, so the theme would never apply. The
|
|
250
|
+
// `every script is loaded from this origin` test in
|
|
251
|
+
// `tests/built-output.test.ts` fails if this is ever raised.
|
|
252
|
+
assetsInlineLimit: 0,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
});
|