cornhsu-polymigrate 1.1.0

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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # PolyMigrate
2
+
3
+ [![NuGet](https://img.shields.io/nuget/v/Cornhsu.PolyMigrate.svg?label=Cornhsu.PolyMigrate)](https://www.nuget.org/packages/Cornhsu.PolyMigrate)
4
+ [![Downloads](https://img.shields.io/nuget/dt/Cornhsu.PolyMigrate.svg)](https://www.nuget.org/packages/Cornhsu.PolyMigrate)
5
+ [![CI](https://github.com/HSU-YU-MING/cornhsu-polymigrate/actions/workflows/ci.yml/badge.svg)](https://github.com/HSU-YU-MING/cornhsu-polymigrate/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ > **The i18n-first static-site migrator** — the only site migrator that pairs your multilingual pages automatically.
9
+
10
+ **[Project write-up](https://cornhsu.com/polymigrate) · [NuGet](https://www.nuget.org/packages/Cornhsu.PolyMigrate) · [繁體中文說明](README.zh-Hant.md) · MIT**
11
+
12
+ PolyMigrate turns legacy dynamic sites (old PHP sites and the like) into clean, static-site-ready
13
+ Markdown — and it treats multilingual content as a first-class concern, not an afterthought.
14
+ Config-driven, fully offline-rerunnable, built on .NET.
15
+
16
+ **Status: 1.0.** The extraction pipeline, pairing, verification, thumbnails and orphan-page
17
+ recovery are complete and validated against a real full-site migration (see below). The CLI
18
+ surface and the Phase output contracts are now stable: new features bump the minor version,
19
+ fixes bump the patch version.
20
+
21
+ ## Why
22
+
23
+ Every multilingual institution site (governments, universities, NGOs, religious organizations)
24
+ faces the same painful migration step: matching up the language versions of every page by hand.
25
+ No existing tool solves this — general-purpose scrapers extract pages one at a time and leave
26
+ the pairing to you. PolyMigrate:
27
+
28
+ - **pairs automatically** where filenames are symmetric (`/ch/news/x` ↔ `/en/news/x` share a
29
+ `translation_key`),
30
+ - **suggests pairs heuristically** where they are not — shared photo albums, normalized dates
31
+ hidden in slugs (`20240121` vs `01212024`), title similarity,
32
+ - **honestly reports what it cannot pair**, producing a review-ready gap inventory instead of
33
+ guessing wrong.
34
+
35
+ Languages are not limited to two: declare any number in `lang_map` and every output
36
+ (frontmatter, inventories, pairing) expands accordingly. All locale output is standard BCP-47.
37
+
38
+ ## Battle-tested defaults
39
+
40
+ The extraction pipeline bakes in fixes for real problems found during a real migration —
41
+ things generic tools and LLM extractors silently get wrong:
42
+
43
+ | Real-world pit | Built-in handling |
44
+ |---|---|
45
+ | Phone photos sideways/upside-down in thumbnails | EXIF auto-orientation before resizing |
46
+ | Titles with colons / numeric slugs with leading zeros | YAML-library escaping, forced quoting |
47
+ | `%20` double-encoding in image paths | decoded on disk, single-encoded in URLs |
48
+ | Markdown converters dropping videos / iframes / PDFs | placeholder round-trip keeps embeds in place |
49
+ | `<title>` polluted with dates and site name | body-first title extraction + configurable cleanup |
50
+ | Mixed date formats (`YYYYMMDD` / `MMDDYYYY` / `DDMMYYYY`) | all recognized and normalized |
51
+ | Old articles removed from indexes but still served | orphan probing (per-day URL candidates + suffix variants) |
52
+ | Broken images on the source site | detected, recorded, never blocks the run |
53
+ | Bot protection (JS cookie challenge → 409) | declarative cookie workaround in config |
54
+ | Legacy encodings (Big5, GB2312, …) | declared or defaulted per site |
55
+
56
+ ## Case study: full temple-site migration
57
+
58
+ PolyMigrate's pipeline is the productized version of a completed real migration
59
+ (a bilingual Buddhist temple site, Chinese/English):
60
+
61
+ - **516 pages** mirrored and extracted, **4.6 GB** of media
62
+ - **281 translation keys**; **231 bilingual articles paired automatically** by symmetric paths
63
+ - built-in verifier: **1,269 internal links + 4,116 media references checked — 0 errors**
64
+ - 13 orphaned articles recovered via date probing; 141 EXIF-rotated photos fixed in thumbnails
65
+
66
+ The original Python prototype's output was used as the golden baseline while porting: 466/516
67
+ extracted bodies are byte-identical after whitespace normalization, and the remainder are
68
+ render-equivalent or strictly more faithful.
69
+
70
+ Two more numbers from that run:
71
+
72
+ - **Re-runs are ~7× faster**: media hashes are cached by `(size, mtime)`, taking a full re-run
73
+ of the 4.6 GB site from **30.1 s down to 4.6 s**.
74
+ - **The output is deployable as-is**: `redirect_map` is auto-filled with the new paths, and
75
+ PolyMigrate emits both an **nginx conf** and a **Netlify `_redirects`** file — turning a
76
+ half-day of hand-written 301s into copying one file.
77
+
78
+ ## Install & use
79
+
80
+ Two channels — pick whichever runtime you already have. **Same tool, same behaviour.**
81
+
82
+ ```
83
+ npx cornhsu-polymigrate extract site.yaml # Node — no .NET install needed
84
+ dotnet tool install -g Cornhsu.PolyMigrate # .NET
85
+ ```
86
+
87
+ > The npm build ships a self-contained native binary and only downloads the one
88
+ > matching your platform (win32-x64 / linux-x64 / darwin-x64 / darwin-arm64).
89
+ > Migrating a site is usually a one-off job — installing a whole SDK for a single
90
+ > run is friction most people won't accept, so the tool meets you where you are.
91
+
92
+ ```
93
+ dotnet tool install -g Cornhsu.PolyMigrate
94
+ polymigrate extract site.yaml # mirror HTML -> frontmatter Markdown + inventories
95
+ polymigrate verify out/ # link/media/frontmatter audit, CI-friendly exit codes
96
+ polymigrate thumbs site.yaml # EXIF-corrected, width-capped thumbnails
97
+ polymigrate probe-orphans site.yaml --section news --years 2021-2023
98
+ polymigrate fetch-orphans site.yaml --section news
99
+ ```
100
+
101
+ One YAML config per site describes everything site-specific — see
102
+ [examples/ibps-austin.yaml](examples/ibps-austin.yaml) for a fully-annotated real example.
103
+
104
+ ```yaml
105
+ config_version: 1
106
+ site:
107
+ base_url: https://legacy.example.org
108
+ url_pattern:
109
+ lang_map: { ch: zh-Hant, en: en } # any number of languages
110
+ default_lang: zh-Hant
111
+ strip_extensions: [.php]
112
+ extract:
113
+ content: "section[id]:not(#header):not(#footer)"
114
+ pairing:
115
+ strategy: symmetric_path
116
+ fallback: [shared_media, date, title_similarity]
117
+ ```
118
+
119
+ ## Layout
120
+
121
+ | Path | Contents |
122
+ |---|---|
123
+ | `src/PolyMigrate.Core` | extraction / pairing / verification library (NuGet: `Cornhsu.PolyMigrate.Core`) |
124
+ | `src/PolyMigrate.Cli` | the `polymigrate` CLI (NuGet tool package: `Cornhsu.PolyMigrate`) |
125
+ | `tests/` | 119 unit/integration tests + an offline fixture site with golden-file baselines |
126
+ | `docs/contracts.md` | file-format contracts between pipeline phases |
127
+
128
+ ## Development
129
+
130
+ ```
131
+ dotnet build
132
+ dotnet test
133
+ dotnet run --project src/PolyMigrate.Cli -- --help
134
+ ```
135
+
136
+ License: [MIT](LICENSE). All dependencies are MIT/BSD/Apache-2.0
137
+ (imaging via **Magick.NET**; ImageSharp was dropped when its 4.x line began requiring a
138
+ license key at build time).
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // npm 通路的啟動腳本。
3
+ //
4
+ // 為什麼要有這一層(而不是直接把執行檔當 bin):
5
+ // npm 靠 optionalDependencies 的 os/cpu 只下載使用者當前平台的那一包,
6
+ // 但執行時仍需要解析出它實際被裝到哪裡。
7
+ //
8
+ // 為什麼要有 npm 通路:PolyMigrate 的使用者是「要把舊網站搬成靜態站的人」,
9
+ // 那些人多半在 Hugo / Eleventy / Astro / Next.js 的生態裡,手邊有 Node、
10
+ // 不一定有 .NET SDK。而且搬站是一次性任務 —— 為了用一次而裝一整套 SDK,
11
+ // 摩擦成本高到多數人會直接放棄。
12
+ //
13
+ // 刻意不做 postinstall 下載:那會被 `npm ci --ignore-scripts` 擋掉,
14
+ // 而不少公司的 CI 預設就是關腳本的。
15
+
16
+ "use strict";
17
+
18
+ const { spawnSync } = require("node:child_process");
19
+ const path = require("node:path");
20
+
21
+ // 平台 → 子套件名 / 執行檔名
22
+ const TARGETS = {
23
+ "win32 x64": { pkg: "@cornhsu/polymigrate-win32-x64", bin: "polymigrate.exe" },
24
+ "linux x64": { pkg: "@cornhsu/polymigrate-linux-x64", bin: "polymigrate" },
25
+ "darwin x64": { pkg: "@cornhsu/polymigrate-darwin-x64", bin: "polymigrate" },
26
+ "darwin arm64": { pkg: "@cornhsu/polymigrate-darwin-arm64", bin: "polymigrate" },
27
+ };
28
+
29
+ const key = `${process.platform} ${process.arch}`;
30
+ const target = TARGETS[key];
31
+
32
+ if (!target) {
33
+ console.error(
34
+ `Cornhsu.PolyMigrate 沒有 ${key} 的預先建置版本。\n` +
35
+ `目前支援:${Object.keys(TARGETS).join("、")}\n` +
36
+ `其他平台請改用 .NET 版本:dotnet tool install -g Cornhsu.PolyMigrate`
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ let binary;
42
+ try {
43
+ // 從子套件的 package.json 反推安裝位置,不用猜 node_modules 的層級
44
+ // (pnpm / yarn 的實體結構與 npm 不同,require.resolve 才可靠)
45
+ const manifest = require.resolve(`${target.pkg}/package.json`);
46
+ binary = path.join(path.dirname(manifest), "bin", target.bin);
47
+ } catch {
48
+ console.error(
49
+ `找不到平台套件 ${target.pkg}。\n` +
50
+ `可能是安裝時加了 --no-optional,或安裝過程中斷。\n` +
51
+ `請重新安裝:npm install cornhsu-polymigrate`
52
+ );
53
+ process.exit(1);
54
+ }
55
+
56
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
57
+
58
+ if (result.error) {
59
+ console.error(`無法執行 ${binary}:${result.error.message}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ // 被訊號中斷時 status 會是 null;轉成慣例的 128+signal,別讓 CI 誤判為成功
64
+ if (result.status === null && result.signal) {
65
+ process.exit(128 + (require("node:os").constants.signals[result.signal] ?? 0));
66
+ }
67
+
68
+ process.exit(result.status ?? 0);
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "cornhsu-polymigrate",
3
+ "version": "1.1.0",
4
+ "description": "The i18n-first static-site migrator — the only one that pairs your multilingual pages automatically. Mirrors a legacy site into frontmatter Markdown, auto-matches language versions, and honestly lists what it could not pair.",
5
+ "keywords": [
6
+ "i18n",
7
+ "internationalization",
8
+ "multilingual",
9
+ "site-migration",
10
+ "static-site",
11
+ "markdown",
12
+ "web-scraping",
13
+ "content-migration",
14
+ "hugo",
15
+ "eleventy",
16
+ "astro"
17
+ ],
18
+ "homepage": "https://cornhsu.com/polymigrate",
19
+ "bugs": "https://github.com/HSU-YU-MING/cornhsu-polymigrate/issues",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/HSU-YU-MING/cornhsu-polymigrate.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "許彧銘 Hsu Yu-Ming (https://cornhsu.com/)",
26
+ "type": "commonjs",
27
+ "bin": {
28
+ "polymigrate": "bin/polymigrate.js"
29
+ },
30
+ "files": [
31
+ "bin/polymigrate.js",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "optionalDependencies": {
38
+ "@cornhsu/polymigrate-win32-x64": "1.1.0",
39
+ "@cornhsu/polymigrate-linux-x64": "1.1.0",
40
+ "@cornhsu/polymigrate-darwin-x64": "1.1.0",
41
+ "@cornhsu/polymigrate-darwin-arm64": "1.1.0"
42
+ }
43
+ }