notion-static-exporter 1.0.2
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/assets.d.ts +47 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/browser.d.ts +10 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/cache.d.ts +54 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/challenge.d.ts +23 -0
- package/dist/challenge.d.ts.map +1 -0
- package/dist/cli.js +5595 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/crawl.d.ts +18 -0
- package/dist/crawl.d.ts.map +1 -0
- package/dist/github.d.ts +17 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/index.cjs +49 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/pwa.d.ts +7 -0
- package/dist/pwa.d.ts.map +1 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/scrape.d.ts +30 -0
- package/dist/scrape.d.ts.map +1 -0
- package/dist/snapshot.d.ts +36 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/ui.d.ts +30 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/urls.d.ts +21 -0
- package/dist/urls.d.ts.map +1 -0
- package/examples/sync-notion.yml +78 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- Initial release: Puppeteer BFS scrape of public `notion.site` pages
|
|
6
|
+
- Freezes live Notion UI (not admin HTML-export styling)
|
|
7
|
+
- Cloudflare handling: wait for clearance, persistent Chrome profile, delays,
|
|
8
|
+
retries, SPA clicks for subpages
|
|
9
|
+
- Downloads CSS/fonts/images; strips client JS for offline static hosting
|
|
10
|
+
- CLI: `sync`, `trigger`, `init-config`, `help`
|
|
11
|
+
- Sample GitHub Actions workflow (cron every 4h + workflow_dispatch)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 novailoveyou
|
|
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,223 @@
|
|
|
1
|
+
# notion-static-exporter
|
|
2
|
+
|
|
3
|
+
Scrape a **public** Notion site (`*.notion.site`) into a static mirror that looks
|
|
4
|
+
like the **live Notion UI** — not Notion’s admin “Export → HTML” look.
|
|
5
|
+
|
|
6
|
+
Compare:
|
|
7
|
+
|
|
8
|
+
- Live UI (target): https://almond-brownie-c82.notion.site/Elementary-3b515e0e4a098053bb74c985cebfd777
|
|
9
|
+
- Old export style (different UI): https://elementary.english.orlov.app/
|
|
10
|
+
|
|
11
|
+
This tool freezes the painted Notion SPA (HTML + CSS + fonts + images), strips
|
|
12
|
+
client JS that would blank the page offline, rewrites links, and writes files
|
|
13
|
+
you can host on GitHub Pages (installable PWA with offline cache).
|
|
14
|
+
|
|
15
|
+
No Notion API token. Exhaustive BFS: every same-site link on every page is
|
|
16
|
+
queued until the tree is done.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx notion-static-exporter sync \
|
|
20
|
+
--url "https://almond-brownie-c82.notion.site/Elementary-3b515e0e4a098053bb74c985cebfd777" \
|
|
21
|
+
--out ./_site
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Publishing
|
|
25
|
+
|
|
26
|
+
Publish this package to npm so Pages repos can call it from GitHub Actions:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# one-time: login to npm (browser or token)
|
|
30
|
+
npm login
|
|
31
|
+
# or: npm config set //registry.npmjs.org/:_authToken=npm_…
|
|
32
|
+
|
|
33
|
+
bun run build
|
|
34
|
+
bun run publish:prod # → bun publish --access public
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That publishes `notion-static-exporter` (version from `package.json`). After that,
|
|
38
|
+
anywhere can run:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx notion-static-exporter sync --url "…" --out .
|
|
42
|
+
# or
|
|
43
|
+
bunx notion-static-exporter sync --url "…" --out .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Bump `version` in `package.json` before each publish if the previous version is
|
|
47
|
+
already on the registry.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Install / run locally
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bunx notion-static-exporter sync --url "https://….notion.site/…" --out ./_site
|
|
55
|
+
bun add -g notion-static-exporter
|
|
56
|
+
notion-static-exporter sync --url "…" --out ./_site
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Optional config `notion-static-exporter.config.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"url": "https://almond-brownie-c82.notion.site/Elementary-3b515e0e4a098053bb74c985cebfd777",
|
|
64
|
+
"out": "./_site",
|
|
65
|
+
"keepCname": true,
|
|
66
|
+
"maxPages": 0
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bunx notion-static-exporter init-config
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Wire each GitHub Pages repo
|
|
75
|
+
|
|
76
|
+
Use the ready template: [`examples/sync-notion.yml`](examples/sync-notion.yml).
|
|
77
|
+
|
|
78
|
+
#### [elementary.english.orlov.app](https://github.com/Novailoveyou/elementary.english.orlov.app)
|
|
79
|
+
|
|
80
|
+
1. Add `.github/workflows/sync-notion.yml` (copy from the example).
|
|
81
|
+
2. Repo **Settings → Secrets and variables → Actions → Variables**:
|
|
82
|
+
- `NOTION_URL` = `https://almond-brownie-c82.notion.site/Elementary-3b515e0e4a098053bb74c985cebfd777`
|
|
83
|
+
3. Ensure **GitHub Pages** serves from `main` (root or `/docs` — whatever you use now).
|
|
84
|
+
4. Keep existing `CNAME` — the workflow uses `--keep-cname`.
|
|
85
|
+
5. Remove / stop the old `publish.yml` (notion4ever + `NOTION_TOKEN`) so you don’t have two deployers fighting.
|
|
86
|
+
|
|
87
|
+
#### [english.orlov.app](https://github.com/Novailoveyou/english.orlov.app)
|
|
88
|
+
|
|
89
|
+
Same workflow file, different variable:
|
|
90
|
+
|
|
91
|
+
- `NOTION_URL` = `https://almond-brownie-c82.notion.site/Elena-Pilip-61ff5f4c44a34429944ea190ee690cc9`
|
|
92
|
+
(drop the `#…` hash for the crawl root)
|
|
93
|
+
|
|
94
|
+
No Notion API token needed — public `*.notion.site` only.
|
|
95
|
+
|
|
96
|
+
### What the Action does
|
|
97
|
+
|
|
98
|
+
On schedule (every 4h in the example) or **Actions → Sync Notion → Run workflow**:
|
|
99
|
+
|
|
100
|
+
1. Checkout the Pages repo
|
|
101
|
+
2. Install Chrome + Bun
|
|
102
|
+
3. Cache Chrome profile (Cloudflare cookies)
|
|
103
|
+
4. `bunx notion-static-exporter sync --url "$NOTION_URL" --out . --keep-cname …`
|
|
104
|
+
5. Commit & push if anything changed → Pages updates
|
|
105
|
+
|
|
106
|
+
The workflow also:
|
|
107
|
+
|
|
108
|
+
- Points Puppeteer at Chrome via `PUPPETEER_EXECUTABLE_PATH`
|
|
109
|
+
- Runs headless with container-safe flags (`--no-sandbox`, `--disable-dev-shm-usage`, …)
|
|
110
|
+
- Uses lower concurrency (`2`) to fit runner memory
|
|
111
|
+
|
|
112
|
+
Local override for a custom Chrome binary:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome \
|
|
116
|
+
bunx notion-static-exporter sync --url "…" --out ./_site
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Trigger from this machine
|
|
120
|
+
|
|
121
|
+
After the workflow exists:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export GITHUB_TOKEN=ghp_… # workflow scope
|
|
125
|
+
bunx notion-static-exporter trigger \
|
|
126
|
+
--repo Novailoveyou/elementary.english.orlov.app \
|
|
127
|
+
--workflow sync-notion.yml \
|
|
128
|
+
--ref main
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Same for `english.orlov.app`.
|
|
132
|
+
|
|
133
|
+
### Order of operations
|
|
134
|
+
|
|
135
|
+
| Step | Where |
|
|
136
|
+
|------|--------|
|
|
137
|
+
| 1. `bun run publish:prod` | `notion-static-exporter` |
|
|
138
|
+
| 2. Add workflow + `NOTION_URL` | each Pages repo |
|
|
139
|
+
| 3. Disable old notion4ever `publish.yml` | each Pages repo |
|
|
140
|
+
| 4. Manual “Run workflow” once | verify scrape + commit |
|
|
141
|
+
| 5. Leave schedule on | ongoing sync |
|
|
142
|
+
|
|
143
|
+
## Commands
|
|
144
|
+
|
|
145
|
+
| Command | Purpose |
|
|
146
|
+
|--------|---------|
|
|
147
|
+
| `sync` | Crawl public URL → write static files (default) |
|
|
148
|
+
| `restore` | Restore `--out` from the previous `.nsp-backup` |
|
|
149
|
+
| `trigger` | `workflow_dispatch` on a consumer GitHub repo |
|
|
150
|
+
| `help` | Usage |
|
|
151
|
+
|
|
152
|
+
### Sync flags
|
|
153
|
+
|
|
154
|
+
- `--url` / `NOTION_URL` — public Notion root
|
|
155
|
+
- `--out` / `OUT_DIR` — output directory (default `.`)
|
|
156
|
+
- `--keep-cname` — keep existing `CNAME` across sync
|
|
157
|
+
- `--max-pages <n>` — safety cap (`0` = unlimited)
|
|
158
|
+
- `--delay-ms <n>` — pause between pages (default `1500`)
|
|
159
|
+
- `--concurrency <n>` — parallel browser tabs (default `3`)
|
|
160
|
+
- `--retries <n>` — retries when Cloudflare blocks (default `3`)
|
|
161
|
+
- `--user-data-dir <path>` — Chrome profile (keeps CF cookies)
|
|
162
|
+
- `--headed` — show Chromium (helps pass hard challenges)
|
|
163
|
+
- `--full` — ignore fingerprints; re-freeze every page
|
|
164
|
+
|
|
165
|
+
### Resync / cache
|
|
166
|
+
|
|
167
|
+
Sync builds into a staging folder, then swaps into `--out` and keeps **one**
|
|
168
|
+
backup (`--out.nsp-backup`). Unchanged pages (content fingerprint match) skip
|
|
169
|
+
freeze/asset download. Use `--full` to force a complete re-scrape. On failure
|
|
170
|
+
the live `--out` is left untouched.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bunx notion-static-exporter restore --out ./_site
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Cloudflare
|
|
177
|
+
|
|
178
|
+
Notion’s public sites sit behind Cloudflare (“Just a moment…”). The crawler:
|
|
179
|
+
|
|
180
|
+
1. Uses a **persistent Chrome profile** (`~/.notion-static-exporter/chrome-profile`) so `cf_clearance` cookies stick across runs
|
|
181
|
+
2. **Waits** for challenges to clear before saving a page
|
|
182
|
+
3. **Delays** between page loads (`--delay-ms`)
|
|
183
|
+
4. **Retries** blocked pages (`--retries`, default 3)
|
|
184
|
+
5. Never saves a challenge interstitial as content
|
|
185
|
+
|
|
186
|
+
If challenges still stick in headless CI, warm the profile once locally:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bunx notion-static-exporter sync --url "…" --out ./_site --headed --max-pages 5
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Then re-run headless using the same `--user-data-dir` (or let Actions restore the cached profile).
|
|
193
|
+
|
|
194
|
+
## How it works
|
|
195
|
+
|
|
196
|
+
1. Launch Chromium (Puppeteer) — system Chrome, `PUPPETEER_EXECUTABLE_PATH`, or bundled
|
|
197
|
+
2. Open each public page and wait for the **live Notion UI** to paint
|
|
198
|
+
3. Collect **all** same-origin page links → BFS queue
|
|
199
|
+
4. Download CSS / fonts / images into `assets/`
|
|
200
|
+
5. **Freeze** the rendered DOM (inline accessible CSS, remove Notion client JS)
|
|
201
|
+
6. Rewrite links to local `.html` files + PWA service worker
|
|
202
|
+
7. Publish via staging → backup → swap
|
|
203
|
+
|
|
204
|
+
CLI shows an animated spinner, a progress bar (`done/known`, grows as the queue
|
|
205
|
+
discovers pages), and rotating status text for the current phase (navigate,
|
|
206
|
+
Cloudflare, views, assets, freeze, …). In GitHub Actions (no TTY) it prints
|
|
207
|
+
phase lines every few seconds instead.
|
|
208
|
+
|
|
209
|
+
## Library
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { syncNotionSite, triggerWorkflow } from "notion-static-exporter";
|
|
213
|
+
|
|
214
|
+
await syncNotionSite({
|
|
215
|
+
url: "https://….notion.site/…",
|
|
216
|
+
out: "./_site",
|
|
217
|
+
keepCname: true,
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
package/dist/assets.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { HTTPResponse, Page } from "puppeteer";
|
|
2
|
+
export type AssetStore = {
|
|
3
|
+
map: Map<string, string>;
|
|
4
|
+
/** relative asset path → source URL(s) */
|
|
5
|
+
reverse: Map<string, string>;
|
|
6
|
+
dir: string;
|
|
7
|
+
outRoot: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function createAssetStore(outRoot: string): AssetStore;
|
|
10
|
+
export declare function normalizeAssetKey(url: string): string;
|
|
11
|
+
/** Notion page URLs must never be saved as static assets. */
|
|
12
|
+
export declare function isNotionPageAssetUrl(url: string): boolean;
|
|
13
|
+
export declare function saveAsset(store: AssetStore, url: string, body: Buffer, contentType: string | null): string | null;
|
|
14
|
+
export declare function downloadUrl(store: AssetStore, url: string, page: Page): Promise<string | null>;
|
|
15
|
+
export declare function attachResponseCollector(page: Page): {
|
|
16
|
+
urls: Set<string>;
|
|
17
|
+
responses: Map<string, HTTPResponse>;
|
|
18
|
+
detach: () => void;
|
|
19
|
+
};
|
|
20
|
+
/** Persist bodies Chrome already fetched (best source for images/fonts). */
|
|
21
|
+
export declare function saveCollectedResponses(store: AssetStore, responses: Map<string, HTTPResponse>): Promise<void>;
|
|
22
|
+
export declare function downloadAssetUrls(store: AssetStore, page: Page, urls: Iterable<string>, concurrency?: number): Promise<void>;
|
|
23
|
+
export declare function collectDomAssetUrls(page: Page): Promise<string[]>;
|
|
24
|
+
export declare function rewriteHtml(html: string, pageLocalPath: string, pageUrl: string, store: AssetStore, pageUrlMap: Map<string, string>): string;
|
|
25
|
+
/** Fix <a href="#"> using the enclosing element's data-block-id when it matches a scraped page. */
|
|
26
|
+
export declare function repairDeadPageLinks(html: string, pageUrlMap: Map<string, string>, pageLocalPath: string): string;
|
|
27
|
+
export declare function rewriteCssFiles(store: AssetStore, pageUrl: string): void;
|
|
28
|
+
export declare function findRemainingRemoteUrls(html: string, baseUrl?: string): string[];
|
|
29
|
+
/** Map Notion block UUID (32-hex) → best local audio/image asset path. */
|
|
30
|
+
export declare function buildBlockAssetIndex(entries: Iterable<[string, string]>): Map<string, {
|
|
31
|
+
audio?: string;
|
|
32
|
+
image?: string;
|
|
33
|
+
imageWidth: number;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Inject <audio>/<img> into empty Notion media shells and replace gif
|
|
37
|
+
* placeholders using assets keyed by ?id=<blockId> in the CDN URL.
|
|
38
|
+
*/
|
|
39
|
+
export declare function injectBlockMedia(html: string, store: AssetStore, pageLocalPath: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Replace 1×1 gif placeholders in Notion bookmark cards with the target
|
|
42
|
+
* page's og:image (downloaded locally). Generic — works for any future
|
|
43
|
+
* bookmark links, not just Wordwall.
|
|
44
|
+
*/
|
|
45
|
+
export declare function enrichBookmarkCovers(html: string, store: AssetStore, pageLocalPath: string): Promise<string>;
|
|
46
|
+
export declare function ensureParentDir(filePath: string): void;
|
|
47
|
+
//# sourceMappingURL=assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Browser, type Page } from "puppeteer";
|
|
2
|
+
export type LaunchBrowserOptions = {
|
|
3
|
+
headless?: boolean;
|
|
4
|
+
/** Persist cookies / CF clearance across runs */
|
|
5
|
+
userDataDir?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function defaultUserDataDir(): string;
|
|
8
|
+
export declare function launchBrowser(opts?: LaunchBrowserOptions): Promise<Browser>;
|
|
9
|
+
export declare function preparePage(browser: Browser): Promise<Page>;
|
|
10
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAGA,OAAkB,EAAE,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAG/D,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AA6CD,wBAAsB,aAAa,CACjC,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,OAAO,CAAC,CA4ClB;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAcjE"}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Page } from "puppeteer";
|
|
2
|
+
import type { AssetStore } from "./assets.ts";
|
|
3
|
+
export declare const CACHE_VERSION: 1;
|
|
4
|
+
export declare const CACHE_FILE = ".nsp-cache.json";
|
|
5
|
+
export type PageCacheEntry = {
|
|
6
|
+
url: string;
|
|
7
|
+
file: string;
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
links: string[];
|
|
10
|
+
};
|
|
11
|
+
export type SyncCache = {
|
|
12
|
+
version: typeof CACHE_VERSION;
|
|
13
|
+
rootUrl: string;
|
|
14
|
+
pages: Record<string, PageCacheEntry>;
|
|
15
|
+
/** Normalized asset URL → relative path under site root (e.g. assets/abc.png) */
|
|
16
|
+
assets: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
export declare function stagingDir(outRoot: string): string;
|
|
19
|
+
export declare function backupDir(outRoot: string): string;
|
|
20
|
+
export declare function emptyCache(rootUrl: string): SyncCache;
|
|
21
|
+
export declare function loadCache(dir: string): SyncCache | null;
|
|
22
|
+
export declare function saveCache(dir: string, cache: SyncCache): void;
|
|
23
|
+
/** Seed asset store from a previous cache so downloads/rewrites can reuse files. */
|
|
24
|
+
export declare function hydrateAssetStore(store: AssetStore, cache: SyncCache): number;
|
|
25
|
+
export declare function snapshotAssets(store: AssetStore): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Light content fingerprint after Notion has rendered.
|
|
28
|
+
* Avoids re-freezing / re-downloading when the page body is unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pageFingerprint(page: Page): Promise<string>;
|
|
31
|
+
export declare function siteHasContent(dir: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Prepare a clean staging directory.
|
|
34
|
+
* Copies assets + cache + a few root files (fast). HTML is copied on demand
|
|
35
|
+
* for cache hits, or written fresh when a page is re-scraped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function prepareStaging(outRoot: string, keepCname?: boolean): {
|
|
38
|
+
staging: string;
|
|
39
|
+
copied: boolean;
|
|
40
|
+
};
|
|
41
|
+
/** Ensure a previously scraped HTML page exists in staging (for cache hits). */
|
|
42
|
+
export declare function ensureCachedPageInStaging(outRoot: string, staging: string, file: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Atomically publish staging → outRoot, keeping one previous copy at backup.
|
|
45
|
+
* Live outRoot is never wiped until staging is complete.
|
|
46
|
+
*/
|
|
47
|
+
export declare function publishStaging(outRoot: string): {
|
|
48
|
+
backup: string | null;
|
|
49
|
+
};
|
|
50
|
+
/** Restore the single backup over outRoot (if present). */
|
|
51
|
+
export declare function restoreBackup(outRoot: string): boolean;
|
|
52
|
+
/** Remove HTML pages in staging that were not part of this crawl. */
|
|
53
|
+
export declare function pruneOrphanHtml(staging: string, keepFiles: Set<string>): number;
|
|
54
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,eAAO,MAAM,aAAa,EAAG,CAAU,CAAC;AACxC,eAAO,MAAM,UAAU,oBAAoB,CAAC;AAE5C,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,OAAO,aAAa,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACtC,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAErD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAWvD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAG7D;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAa7E;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBjE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAUnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,OAAO,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAsCtC;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAQT;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA+BzE;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CA4BtD;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,MAAM,CASR"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Page } from "puppeteer";
|
|
2
|
+
/** Cloudflare / bot interstitial ("Just a moment..."). */
|
|
3
|
+
export declare function isChallengePage(page: Page): Promise<boolean>;
|
|
4
|
+
/**
|
|
5
|
+
* Wait until Cloudflare clears (cf_clearance / title change) or timeout.
|
|
6
|
+
* Returns true if the page looks clear of the challenge.
|
|
7
|
+
*/
|
|
8
|
+
export declare function waitOutChallenge(page: Page, timeoutMs?: number): Promise<boolean>;
|
|
9
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
10
|
+
/** Human-like pause between navigations. Uses small jitter; skip when baseMs ≤ 0. */
|
|
11
|
+
export declare function crawlDelay(baseMs: number): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Global spacing between navigations across workers (avoids N×delay with concurrency).
|
|
14
|
+
*/
|
|
15
|
+
export declare function createNavGate(baseMs: number): () => Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Navigate to a Notion page. Prefer in-app link click (SPA) to avoid
|
|
18
|
+
* re-triggering Cloudflare; fall back to full goto.
|
|
19
|
+
*/
|
|
20
|
+
export declare function navigateToPage(page: Page, url: string, opts?: {
|
|
21
|
+
allowSpaClick?: boolean;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=challenge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAItC,0DAA0D;AAC1D,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAqBlE;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,SAAS,SAAS,GACjB,OAAO,CAAC,OAAO,CAAC,CAyBlB;AAED,wBAAsB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAErD;AAED,qFAAqF;AACrF,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAUjE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,OAAO,CAAC,IAAI,CAAC,CAsCf"}
|