for-humanity 0.2.0 → 0.2.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/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/dist/cli.js +19 -8
- package/package.json +1 -1
- package/src/cli.ts +6 -3
- package/src/component/widget/shell/wg-shell.tsx +3 -3
- package/src/constant/asset.ts +2 -2
- package/src/content/read-docs.test.ts +15 -3
- package/src/type/site-assets.ts +8 -0
- package/src/util/file/to-asset-path.ts +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 — 2026-10-04
|
|
4
|
+
|
|
5
|
+
- Fingerprint stylesheet and browser-script URLs so new pages cannot reuse assets cached from an older deployment.
|
|
6
|
+
- Cache fingerprinted CSS, scripts and fonts with immutable headers on Cloudflare Pages.
|
|
7
|
+
|
|
3
8
|
## 0.2.0 — 2026-10-04
|
|
4
9
|
|
|
5
10
|
- Support nested navigation with `group` name arrays while keeping single-name groups compatible.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
|
|
|
33
33
|
Node.js 22 or later and pnpm. Install the CLI in your document project:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add -D --save-exact for-humanity@0.2.
|
|
36
|
+
pnpm add -D --save-exact for-humanity@0.2.1
|
|
37
37
|
mkdir docs
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
|
|
|
52
52
|
|
|
53
53
|
If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
|
|
54
54
|
|
|
55
|
-
Version `0.2.
|
|
55
|
+
Version `0.2.1` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
|
|
56
56
|
Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
|
|
57
57
|
|
|
58
58
|
### Develop the kit
|
|
@@ -74,7 +74,7 @@ pnpm preview # Preview the static site
|
|
|
74
74
|
pnpm build:pages # Add Cloudflare Pages headers and a 404 page
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.2.
|
|
77
|
+
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.2.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
|
|
78
78
|
The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
|
|
79
79
|
|
|
80
80
|
## Make it yours
|
package/dist/cli.js
CHANGED
|
@@ -227,7 +227,7 @@ var WgShell = (props) => {
|
|
|
227
227
|
/* @__PURE__ */ jsx5("style", { dangerouslySetInnerHTML: { __html: props.assets.fontCss } }),
|
|
228
228
|
props.assets.preload.map((href) => /* @__PURE__ */ jsx5("link", { rel: "preload", href, as: "font", type: "font/woff2", crossOrigin: "" }, href)),
|
|
229
229
|
/* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: 'document.documentElement.style.scrollBehavior="auto"' } }),
|
|
230
|
-
/* @__PURE__ */ jsx5("link", { rel: "stylesheet", href:
|
|
230
|
+
/* @__PURE__ */ jsx5("link", { rel: "stylesheet", href: props.assets.stylePath }),
|
|
231
231
|
props.assets.reload && /* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: reloadScript } })
|
|
232
232
|
] }),
|
|
233
233
|
/* @__PURE__ */ jsxs5("body", { className: clsx5("wg_shell__root"), children: [
|
|
@@ -261,7 +261,7 @@ var WgShell = (props) => {
|
|
|
261
261
|
/* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__drawerContent"), "data-navigation-content": "" })
|
|
262
262
|
] }),
|
|
263
263
|
/* @__PURE__ */ jsx5("img", { className: clsx5("wg_shell__cursorFace"), src: asset_favicon_path, width: "24", height: "24", alt: "", "aria-hidden": "true", draggable: false, "data-cursor-face": "" }),
|
|
264
|
-
/* @__PURE__ */ jsx5("script", { type: "module", src:
|
|
264
|
+
/* @__PURE__ */ jsx5("script", { type: "module", src: props.assets.clientPath })
|
|
265
265
|
] })
|
|
266
266
|
] });
|
|
267
267
|
};
|
|
@@ -1169,17 +1169,26 @@ var toErrorMessage = (error) => {
|
|
|
1169
1169
|
return error instanceof Error ? error.message : String(error);
|
|
1170
1170
|
};
|
|
1171
1171
|
|
|
1172
|
-
// src/util/
|
|
1172
|
+
// src/util/file/to-asset-path.ts
|
|
1173
1173
|
import { createHash } from "node:crypto";
|
|
1174
1174
|
import { readFileSync } from "node:fs";
|
|
1175
|
+
import { posix as posix2 } from "node:path";
|
|
1176
|
+
var toAssetPath = (options) => {
|
|
1177
|
+
const extension = posix2.extname(options.path);
|
|
1178
|
+
return `${posix2.dirname(options.path)}/${posix2.basename(options.path, extension)}.${createHash("sha256").update(readFileSync(options.file)).digest("hex")}${extension}`;
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// src/util/font/to-font-css.ts
|
|
1182
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1183
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1175
1184
|
import { basename, dirname as dirname2, extname as extname2, join as join3 } from "node:path";
|
|
1176
1185
|
var toFontCss = (options) => {
|
|
1177
1186
|
const files = /* @__PURE__ */ new Map();
|
|
1178
|
-
const css = [...
|
|
1187
|
+
const css = [...readFileSync2(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
|
|
1179
1188
|
(match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
|
|
1180
1189
|
const file = join3(dirname2(options.css), relativeUrl);
|
|
1181
1190
|
const extension = extname2(file);
|
|
1182
|
-
const hash =
|
|
1191
|
+
const hash = createHash2("sha256").update(readFileSync2(file)).digest("hex").slice(0, font_hash_length);
|
|
1183
1192
|
const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
|
|
1184
1193
|
files.set(url, file);
|
|
1185
1194
|
return `url(${url})`;
|
|
@@ -1228,9 +1237,11 @@ var main = async () => {
|
|
|
1228
1237
|
throw new Error(`${copy_error_font_preload}: ${font_sans_preload_file}`);
|
|
1229
1238
|
}
|
|
1230
1239
|
const fontCss = [sans.css, mono.css, brand.css].join("\n");
|
|
1240
|
+
const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
|
|
1241
|
+
const clientPath = toAssetPath({ path: asset_client_path, file: join4(kitRoot, "dist/client.js") });
|
|
1231
1242
|
const files = new Map([
|
|
1232
|
-
[
|
|
1233
|
-
[
|
|
1243
|
+
[stylePath, join4(kitRoot, "dist/cli.css")],
|
|
1244
|
+
[clientPath, join4(kitRoot, "dist/client.js")],
|
|
1234
1245
|
[asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
|
|
1235
1246
|
...sans.files,
|
|
1236
1247
|
...mono.files,
|
|
@@ -1242,7 +1253,7 @@ var main = async () => {
|
|
|
1242
1253
|
const app = createApp({
|
|
1243
1254
|
site: siteConfig,
|
|
1244
1255
|
store,
|
|
1245
|
-
assets: { fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
|
|
1256
|
+
assets: { stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
|
|
1246
1257
|
});
|
|
1247
1258
|
if (command === "build") {
|
|
1248
1259
|
await rm(outDir, { recursive: true, force: true });
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {readHome} from "@/content/read-home";
|
|
|
25
25
|
import {createDevApp} from "@/dev";
|
|
26
26
|
import {siteConfigSchema} from "@/type/site-config";
|
|
27
27
|
import {toErrorMessage} from "@/util/error/to-error-message";
|
|
28
|
+
import {toAssetPath} from "@/util/file/to-asset-path";
|
|
28
29
|
import {toFontCss} from "@/util/font/to-font-css";
|
|
29
30
|
|
|
30
31
|
const commands = ["dev", "build", "preview"];
|
|
@@ -82,9 +83,11 @@ const main = async () => {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
const fontCss = [sans.css, mono.css, brand.css].join("\n");
|
|
86
|
+
const stylePath = toAssetPath({path: asset_style_path, file: join(kitRoot, "dist/cli.css")});
|
|
87
|
+
const clientPath = toAssetPath({path: asset_client_path, file: join(kitRoot, "dist/client.js")});
|
|
85
88
|
const files = new Map([
|
|
86
|
-
[
|
|
87
|
-
[
|
|
89
|
+
[stylePath, join(kitRoot, "dist/cli.css")],
|
|
90
|
+
[clientPath, join(kitRoot, "dist/client.js")],
|
|
88
91
|
[asset_favicon_path, join(kitRoot, "src/asset/favicon.svg")],
|
|
89
92
|
...sans.files,
|
|
90
93
|
...mono.files,
|
|
@@ -96,7 +99,7 @@ const main = async () => {
|
|
|
96
99
|
const app = createApp({
|
|
97
100
|
site: siteConfig,
|
|
98
101
|
store,
|
|
99
|
-
assets: {fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
|
|
102
|
+
assets: {stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
if (command === "build") {
|
|
@@ -4,7 +4,7 @@ import {toTocGroups} from "@/component/widget/shell/_function/to-toc-groups";
|
|
|
4
4
|
import type {DocOutline} from "@/component/widget/shell/_type/doc-outline";
|
|
5
5
|
import {WgShellNav} from "@/component/widget/shell/_wg-shell-nav";
|
|
6
6
|
import {WgShellToc} from "@/component/widget/shell/_wg-shell-toc";
|
|
7
|
-
import {
|
|
7
|
+
import {asset_favicon_path, asset_reload_path} from "@/constant/asset";
|
|
8
8
|
import {copy_nav_close, copy_nav_open, copy_nav_title} from "@/constant/copy";
|
|
9
9
|
import type {Doc} from "@/type/doc";
|
|
10
10
|
import type {SiteAssets} from "@/type/site-assets";
|
|
@@ -71,7 +71,7 @@ export const WgShell = (props: WgShellProps) => {
|
|
|
71
71
|
<link key={href} rel="preload" href={href} as="font" type="font/woff2" crossOrigin="" />
|
|
72
72
|
))}
|
|
73
73
|
<script dangerouslySetInnerHTML={{__html: 'document.documentElement.style.scrollBehavior="auto"'}} />
|
|
74
|
-
<link rel="stylesheet" href={
|
|
74
|
+
<link rel="stylesheet" href={props.assets.stylePath} />
|
|
75
75
|
{props.assets.reload && <script dangerouslySetInnerHTML={{__html: reloadScript}} />}
|
|
76
76
|
</head>
|
|
77
77
|
{/**
|
|
@@ -140,7 +140,7 @@ export const WgShell = (props: WgShellProps) => {
|
|
|
140
140
|
<div className={clsx("wg_shell__drawerContent")} data-navigation-content="" />
|
|
141
141
|
</dialog>
|
|
142
142
|
<img className={clsx("wg_shell__cursorFace")} src={asset_favicon_path} width="24" height="24" alt="" aria-hidden="true" draggable={false} data-cursor-face="" />
|
|
143
|
-
<script type="module" src={
|
|
143
|
+
<script type="module" src={props.assets.clientPath} />
|
|
144
144
|
</body>
|
|
145
145
|
</html>
|
|
146
146
|
);
|
package/src/constant/asset.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
export const asset_dir = "_fh";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* esbuild의
|
|
11
|
+
* 내용 지문을 붙일 esbuild CSS의 기본 URL
|
|
12
12
|
*/
|
|
13
13
|
export const asset_style_path = `/${asset_dir}/style.css`;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* 테마·목차·hash 보정
|
|
16
|
+
* 내용 지문을 붙일 테마·목차·hash 보정 스크립트의 기본 URL
|
|
17
17
|
*/
|
|
18
18
|
export const asset_client_path = `/${asset_dir}/client.js`;
|
|
19
19
|
|
|
@@ -6,6 +6,7 @@ import {test} from "node:test";
|
|
|
6
6
|
import {renderToStaticMarkup} from "react-dom/server";
|
|
7
7
|
import {createApp} from "@/app";
|
|
8
8
|
import {WgShellNav} from "@/component/widget/shell/_wg-shell-nav";
|
|
9
|
+
import {asset_client_path, asset_style_path} from "@/constant/asset";
|
|
9
10
|
import {createProcessor} from "@/content/create-processor";
|
|
10
11
|
import {readDocs} from "@/content/read-docs";
|
|
11
12
|
import {readHome} from "@/content/read-home";
|
|
@@ -13,6 +14,17 @@ import {toDocGroups} from "@/content/to-doc-groups/to-doc-groups";
|
|
|
13
14
|
import {docDataSchema} from "@/type/doc-data";
|
|
14
15
|
import {siteConfigSchema} from "@/type/site-config";
|
|
15
16
|
|
|
17
|
+
test("page resource URLs use the supplied content fingerprints instead of fixed cache keys", async () => {
|
|
18
|
+
const site = siteConfigSchema.parse({});
|
|
19
|
+
const assets = {fontCss: "", preload: [], reload: false, stylePath: "/_fh/style.abc123.css", clientPath: "/_fh/client.def456.js"};
|
|
20
|
+
const app = createApp({site, store: {docs: []}, assets});
|
|
21
|
+
const html = await (await app.request("/")).text();
|
|
22
|
+
|
|
23
|
+
assert.match(html, /rel="stylesheet" href="\/_fh\/style\.abc123\.css"/);
|
|
24
|
+
assert.match(html, /type="module" src="\/_fh\/client\.def456\.js"/);
|
|
25
|
+
assert.doesNotMatch(html, /(?:href|src)="\/_fh\/(?:style\.css|client\.js)"/);
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
test("documents with the same initial retain distinct URLs", async (t) => {
|
|
17
29
|
const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
|
|
18
30
|
const site = siteConfigSchema.parse({});
|
|
@@ -201,7 +213,7 @@ test("a plain root README renders at home and relative home links resolve from n
|
|
|
201
213
|
]);
|
|
202
214
|
|
|
203
215
|
const [docs, home] = await Promise.all([readDocs({root, processor}), readHome({root, processor})]);
|
|
204
|
-
const app = createApp({site, store: {docs, home}, assets: {fontCss: "", preload: [], reload: false}});
|
|
216
|
+
const app = createApp({site, store: {docs, home}, assets: {stylePath: asset_style_path, clientPath: asset_client_path, fontCss: "", preload: [], reload: false}});
|
|
205
217
|
const response = await app.request("/");
|
|
206
218
|
const html = await response.text();
|
|
207
219
|
|
|
@@ -230,7 +242,7 @@ test("a missing README provides a creation hint without generated overview cards
|
|
|
230
242
|
|
|
231
243
|
t.after(() => rm(root, {recursive: true, force: true}));
|
|
232
244
|
const home = await readHome({root, processor: createProcessor({site, root})});
|
|
233
|
-
const app = createApp({site, store: {docs: [], home}, assets: {fontCss: "", preload: [], reload: false}});
|
|
245
|
+
const app = createApp({site, store: {docs: [], home}, assets: {stylePath: asset_style_path, clientPath: asset_client_path, fontCss: "", preload: [], reload: false}});
|
|
234
246
|
const html = await (await app.request("/")).text();
|
|
235
247
|
|
|
236
248
|
assert.equal(home, undefined);
|
|
@@ -309,7 +321,7 @@ test("headings and contents preserve authored text without automatic numbering",
|
|
|
309
321
|
);
|
|
310
322
|
|
|
311
323
|
const docs = await readDocs({root, processor: createProcessor({site, root})});
|
|
312
|
-
const app = createApp({site, store: {docs}, assets: {fontCss: "", preload: [], reload: false}});
|
|
324
|
+
const app = createApp({site, store: {docs}, assets: {stylePath: asset_style_path, clientPath: asset_client_path, fontCss: "", preload: [], reload: false}});
|
|
313
325
|
const html = await (await app.request("/guide/")).text();
|
|
314
326
|
|
|
315
327
|
assert.match(html, /<h2[^>]*id="overview"[^>]*><span class="wg_prose__headingText">Overview<\/span><\/h2>/);
|
package/src/type/site-assets.ts
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {createHash} from "node:crypto";
|
|
2
|
+
import {readFileSync} from "node:fs";
|
|
3
|
+
import {posix} from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 파일 내용으로 자원 URL을 구분하는 입력
|
|
7
|
+
*/
|
|
8
|
+
export interface AssetPathOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 사이트 루트 기준 기본 자원 경로
|
|
11
|
+
*/
|
|
12
|
+
path: string;
|
|
13
|
+
/**
|
|
14
|
+
* 배포할 실제 파일 경로
|
|
15
|
+
*/
|
|
16
|
+
file: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 파일명에 내용 지문 추가 · 같은 내용은 같은 URL, 변경한 내용은 새 캐시 키 사용
|
|
21
|
+
*/
|
|
22
|
+
export const toAssetPath = (options: AssetPathOptions) => {
|
|
23
|
+
const extension = posix.extname(options.path);
|
|
24
|
+
|
|
25
|
+
return `${posix.dirname(options.path)}/${posix.basename(options.path, extension)}.${createHash("sha256").update(readFileSync(options.file)).digest("hex")}${extension}`;
|
|
26
|
+
};
|