create-zudo-doc 5.13.1 → 5.14.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/CHANGELOG.md +15 -0
- package/README.md +1 -0
- package/dist/claude-md-gen.js +3 -3
- package/dist/constants.js +7 -0
- package/dist/features/asset-viewer.d.ts +10 -0
- package/dist/features/asset-viewer.js +12 -0
- package/dist/features/doc-tags.d.ts +2 -2
- package/dist/features/doc-tags.js +2 -2
- package/dist/features/i18n.d.ts +2 -2
- package/dist/features/i18n.js +2 -2
- package/dist/features/index.js +2 -0
- package/dist/features/versioning.d.ts +3 -6
- package/dist/features/versioning.js +3 -6
- package/dist/scaffold.d.ts +1 -1
- package/dist/scaffold.js +43 -36
- package/dist/zfb-config-gen.js +11 -0
- package/package.json +1 -1
- package/templates/base/pages/docs/[[...slug]].tsx +6 -8
- package/templates/base/scripts/check-links.js +80 -23
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `create-zudo-doc` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
|
|
6
6
|
|
|
7
|
+
## [5.14.0] - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Added the `--asset-viewer` feature flag. Generated projects now emit `assetViewer: true` in `zfb.config.ts`, enabling the package-owned viewer for files under `public/assets/` without adding a host route stub or extra template files. (`45dc50cdb`)
|
|
12
|
+
- Generated doc-history projects now use the package-owned `run-parallel` command for their concurrent development servers, removing `npm-run-all2` and reporting unsupported trailing arguments instead of silently ignoring them. (`9402ff157`, `3ebfa6e9c`)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- Updated the generated link checker to recognize valid unquoted HTML attributes, decode character references, resolve percent-encoded routes, and avoid serialized-markup false positives. (`db9a07018`)
|
|
17
|
+
|
|
18
|
+
### Other Changes
|
|
19
|
+
|
|
20
|
+
- Updated newly generated projects to use the zfb 2.14.0 package family and `@takazudo/zdtp` 0.4.14. (`dbe8553c7`, `038870c94`)
|
|
21
|
+
|
|
7
22
|
## [5.13.1] - 2026-08-28
|
|
8
23
|
|
|
9
24
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Each feature has a `--[no-]<flag>` form. Passing `--feature` enables it; `--no-f
|
|
|
76
76
|
| `--[no-]search` | Full-text search | on |
|
|
77
77
|
| `--[no-]sidebar-filter` | Real-time sidebar filter | on |
|
|
78
78
|
| `--[no-]image-enlarge` | Click-to-enlarge for oversized images | on |
|
|
79
|
+
| `--[no-]asset-viewer` | Viewer pages for files under `public/assets/` | off |
|
|
79
80
|
| `--[no-]tag-governance` | Vocabulary-aware tag audit + suggest scripts | off |
|
|
80
81
|
| `--[no-]claude-resources` | Auto-generate Claude Code docs from `.claude/` | off |
|
|
81
82
|
| `--[no-]codex-resources` | Auto-generate Codex docs from `.codex/` + `AGENTS.md` | off |
|
package/dist/claude-md-gen.js
CHANGED
|
@@ -29,10 +29,10 @@ export function generateCLAUDEFile(choices) {
|
|
|
29
29
|
lines.push(``);
|
|
30
30
|
const pm = choices.packageManager;
|
|
31
31
|
if (choices.features.includes("docHistory")) {
|
|
32
|
-
lines.push(`- \`${pmRunCommand(pm, "dev")}\` — runs the zfb dev server (port 4321) and the doc-history API server (port 4322) concurrently via \`run-
|
|
32
|
+
lines.push(`- \`${pmRunCommand(pm, "dev")}\` — runs the zfb dev server (port 4321) and the doc-history API server (port 4322) concurrently via \`run-parallel\` (a bin from @takazudo/zudo-doc) (\`${pmRunCommand(pm, "dev:zfb")}\` / \`${pmRunCommand(pm, "dev:history")}\` individually)`);
|
|
33
33
|
lines.push(`- \`${pmRunCommand(pm, "dev:network")}\` — same, but zfb binds \`--host 0.0.0.0\` for LAN access (\`${pmRunCommand(pm, "dev:zfb:network")}\` individually); the doc-history server stays loopback-only and LAN clients reach it through zfb's \`/doc-history/*\` dev proxy`);
|
|
34
34
|
lines.push(`- **Trusted networks only:** this also serves your git doc-history — including UNPUBLISHED local commits — to anyone on the LAN via the \`/doc-history/*\` proxy`);
|
|
35
|
-
lines.push(`- \`run-
|
|
35
|
+
lines.push(`- \`run-parallel\` does not forward trailing args — it rejects them with an error rather than silently ignoring them, so pass other zfb flags directly instead: \`${pm} run dev:zfb -- <flags>\``);
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
lines.push(`- \`${pmRunCommand(pm, "dev")}\` — zfb dev server (port 4321)`);
|
|
@@ -114,7 +114,7 @@ export function generateCLAUDEFile(choices) {
|
|
|
114
114
|
lines.push(`- ${defaultLabel} (default): \`/docs/...\` — content in \`src/content/docs/\``);
|
|
115
115
|
lines.push(`- ${secondaryLabel}: \`/${secondaryLang}/docs/...\` — content in \`src/content/docs-${secondaryLang}/\``);
|
|
116
116
|
lines.push(`- ${secondaryLabel} docs should mirror the ${defaultLabel} directory structure`);
|
|
117
|
-
lines.push(`- Both \`pages/docs/[[...slug]].tsx\` and \`pages/[locale]/docs/[[...slug]].tsx\` are self-contained doc-route stubs shipped by the generator
|
|
117
|
+
lines.push(`- Both \`pages/docs/[[...slug]].tsx\` and \`pages/[locale]/docs/[[...slug]].tsx\` are self-contained doc-route stubs shipped by the generator as explicit host-owned seams. zfb 2.13.1 also renders package-injected dynamic routes in dev; keep these files so the generated project retains route ownership and customization.`);
|
|
118
118
|
lines.push(``);
|
|
119
119
|
}
|
|
120
120
|
// Enabled features
|
package/dist/constants.js
CHANGED
|
@@ -331,6 +331,13 @@ export const FEATURES = [
|
|
|
331
331
|
default: true,
|
|
332
332
|
cliFlag: "image-enlarge",
|
|
333
333
|
},
|
|
334
|
+
{
|
|
335
|
+
value: "assetViewer",
|
|
336
|
+
label: "Asset viewer",
|
|
337
|
+
hint: "Viewer pages for files under public/assets",
|
|
338
|
+
default: false,
|
|
339
|
+
cliFlag: "asset-viewer",
|
|
340
|
+
},
|
|
334
341
|
{
|
|
335
342
|
value: "dynamicPageTransition",
|
|
336
343
|
label: "Dynamic page transition",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FeatureModule } from "../compose.js";
|
|
2
|
+
/**
|
|
3
|
+
* Asset-viewer feature.
|
|
4
|
+
*
|
|
5
|
+
* Purely a `zudoDoc({ assetViewer: true })` field (see `zfb-config-gen.ts`).
|
|
6
|
+
* The scanner, viewer route, and all supporting UI are package-owned by
|
|
7
|
+
* `@takazudo/zudo-doc`; generated projects only need the config toggle.
|
|
8
|
+
* Nothing is injected or copied into the scaffold.
|
|
9
|
+
*/
|
|
10
|
+
export declare const assetViewerFeature: FeatureModule;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset-viewer feature.
|
|
3
|
+
*
|
|
4
|
+
* Purely a `zudoDoc({ assetViewer: true })` field (see `zfb-config-gen.ts`).
|
|
5
|
+
* The scanner, viewer route, and all supporting UI are package-owned by
|
|
6
|
+
* `@takazudo/zudo-doc`; generated projects only need the config toggle.
|
|
7
|
+
* Nothing is injected or copied into the scaffold.
|
|
8
|
+
*/
|
|
9
|
+
export const assetViewerFeature = () => ({
|
|
10
|
+
name: "assetViewer",
|
|
11
|
+
injections: [],
|
|
12
|
+
});
|
|
@@ -9,7 +9,7 @@ import type { FeatureModule } from "../compose.js";
|
|
|
9
9
|
* since the host catch-all stubs were retired in favor of package
|
|
10
10
|
* injection — there is nothing left to copy or postProcess.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* zfb 2.13.1 renders these injected tag routes in both dev and build. This
|
|
13
|
+
* feature has no host stubs because package injection owns the routes.
|
|
14
14
|
*/
|
|
15
15
|
export declare const docTagsFeature: FeatureModule;
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* since the host catch-all stubs were retired in favor of package
|
|
9
9
|
* injection — there is nothing left to copy or postProcess.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* zfb 2.13.1 renders these injected tag routes in both dev and build. This
|
|
12
|
+
* feature has no host stubs because package injection owns the routes.
|
|
13
13
|
*/
|
|
14
14
|
export const docTagsFeature = () => ({
|
|
15
15
|
name: "docTags",
|
package/dist/features/i18n.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { FeatureModule } from "../compose.js";
|
|
|
4
4
|
*
|
|
5
5
|
* Locked manifest (#2653 Decision 4, i18n addendum): "i18n ON adds
|
|
6
6
|
* `pages/[locale]/docs/[[...slug]].tsx` (a second doc stub, locale variant)
|
|
7
|
-
* … No other pages." That ONE self-contained stub —
|
|
8
|
-
*
|
|
7
|
+
* … No other pages." That ONE self-contained stub — retained as an explicit
|
|
8
|
+
* host-owned route seam; zfb 2.13.1 also serves injected dynamic routes in dev —
|
|
9
9
|
* is shipped under `templates/features/i18n/files/pages/[locale]/docs/`
|
|
10
10
|
* and copied by `composeFeatures → copyFeatureFiles` whenever `i18n` is
|
|
11
11
|
* selected. The old `pages/[locale]/index.tsx` home-route template is GONE
|
package/dist/features/i18n.js
CHANGED
|
@@ -6,8 +6,8 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
*
|
|
7
7
|
* Locked manifest (#2653 Decision 4, i18n addendum): "i18n ON adds
|
|
8
8
|
* `pages/[locale]/docs/[[...slug]].tsx` (a second doc stub, locale variant)
|
|
9
|
-
* … No other pages." That ONE self-contained stub —
|
|
10
|
-
*
|
|
9
|
+
* … No other pages." That ONE self-contained stub — retained as an explicit
|
|
10
|
+
* host-owned route seam; zfb 2.13.1 also serves injected dynamic routes in dev —
|
|
11
11
|
* is shipped under `templates/features/i18n/files/pages/[locale]/docs/`
|
|
12
12
|
* and copied by `composeFeatures → copyFeatureFiles` whenever `i18n` is
|
|
13
13
|
* selected. The old `pages/[locale]/index.tsx` home-route template is GONE
|
package/dist/features/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { versioningFeature } from "./versioning.js";
|
|
|
22
22
|
import { tauriFeature } from "./tauri.js";
|
|
23
23
|
import { tauriDevFeature } from "./tauri-dev.js";
|
|
24
24
|
import { imageEnlargeFeature } from "./image-enlarge.js";
|
|
25
|
+
import { assetViewerFeature } from "./asset-viewer.js";
|
|
25
26
|
import { dynamicPageTransitionFeature } from "./dynamic-page-transition.js";
|
|
26
27
|
import { tagGovernanceFeature } from "./tag-governance.js";
|
|
27
28
|
import { footerTaglistFeature } from "./footer-taglist.js";
|
|
@@ -50,6 +51,7 @@ export const featureModules = {
|
|
|
50
51
|
tauri: tauriFeature,
|
|
51
52
|
tauriDev: tauriDevFeature,
|
|
52
53
|
imageEnlarge: imageEnlargeFeature,
|
|
54
|
+
assetViewer: assetViewerFeature,
|
|
53
55
|
dynamicPageTransition: dynamicPageTransitionFeature,
|
|
54
56
|
// skillSymlinker — handled in scaffold.ts
|
|
55
57
|
// claudeSkills — handled in scaffold.ts (copies zudo-doc-* skills from monorepo)
|
|
@@ -9,11 +9,8 @@ import type { FeatureModule } from "../compose.js";
|
|
|
9
9
|
* `renderDocPage` chrome. `templates/features/versioning/files/` has been
|
|
10
10
|
* empty since #2390 — there is no host stub left to copy or postProcess.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* primary doc route, tracked as a #2667 follow-up): the versioned doc
|
|
16
|
-
* routes may still 404 in `zfb dev` since versioning has no stub of its own
|
|
17
|
-
* in the locked manifest. `zfb build` is unaffected.
|
|
12
|
+
* zfb 2.13.1 renders these injected version routes in both dev and build, so
|
|
13
|
+
* versioning needs no version-specific host stub. The primary and locale doc
|
|
14
|
+
* stubs remain explicit host-owned seams.
|
|
18
15
|
*/
|
|
19
16
|
export declare const versioningFeature: FeatureModule;
|
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
* `renderDocPage` chrome. `templates/features/versioning/files/` has been
|
|
9
9
|
* empty since #2390 — there is no host stub left to copy or postProcess.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* primary doc route, tracked as a #2667 follow-up): the versioned doc
|
|
15
|
-
* routes may still 404 in `zfb dev` since versioning has no stub of its own
|
|
16
|
-
* in the locked manifest. `zfb build` is unaffected.
|
|
11
|
+
* zfb 2.13.1 renders these injected version routes in both dev and build, so
|
|
12
|
+
* versioning needs no version-specific host stub. The primary and locale doc
|
|
13
|
+
* stubs remain explicit host-owned seams.
|
|
17
14
|
*/
|
|
18
15
|
export const versioningFeature = () => ({
|
|
19
16
|
name: "versioning",
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -32,5 +32,5 @@ export declare function deriveDocSkillName(projectName: string): string;
|
|
|
32
32
|
*
|
|
33
33
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
34
34
|
*/
|
|
35
|
-
export declare const ZUDO_DOC_PIN = "^5.
|
|
35
|
+
export declare const ZUDO_DOC_PIN = "^5.14.0";
|
|
36
36
|
export declare function scaffold(choices: UserChoices): Promise<void>;
|
package/dist/scaffold.js
CHANGED
|
@@ -43,7 +43,7 @@ export function deriveDocSkillName(projectName) {
|
|
|
43
43
|
*
|
|
44
44
|
* Bumped in lockstep by scripts/release-create-zudo-doc.sh.
|
|
45
45
|
*/
|
|
46
|
-
export const ZUDO_DOC_PIN = "^5.
|
|
46
|
+
export const ZUDO_DOC_PIN = "^5.14.0";
|
|
47
47
|
/**
|
|
48
48
|
* Files in `templates/base/**` that must not be copied by the unconditional
|
|
49
49
|
* base mirror. Each entry is matched against the path relative to
|
|
@@ -715,15 +715,15 @@ function generatePackageJson(choices) {
|
|
|
715
715
|
// one CSS entrypoint without route discovery or page rendering, and fixes
|
|
716
716
|
// a Back race before ClientRouter DOM swaps. Both changes are additive
|
|
717
717
|
// tooling/runtime behavior; a fresh scaffold needs no config migration.
|
|
718
|
-
// 2.
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
"@takazudo/zfb": "2.
|
|
725
|
-
"@takazudo/zfb-runtime": "2.
|
|
726
|
-
"@takazudo/zfb-md-wasm": "2.
|
|
718
|
+
// 2.14.0: additive behavior and bug fixes. zfb can bundle authored CSS
|
|
719
|
+
// imports when Tailwind is off, scans claimed root-package CSS sources,
|
|
720
|
+
// and adds the optional `strictPlainCssImports` flag (default false).
|
|
721
|
+
// Parser fixes also ship in zfb-md-wasm; runtime and the Cloudflare adapter
|
|
722
|
+
// have no package-specific behavior change. Public exports and engine
|
|
723
|
+
// requirements stay unchanged, so a fresh scaffold needs no migration.
|
|
724
|
+
"@takazudo/zfb": "2.14.0",
|
|
725
|
+
"@takazudo/zfb-runtime": "2.14.0",
|
|
726
|
+
"@takazudo/zfb-md-wasm": "2.14.0",
|
|
727
727
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
728
728
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
729
729
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
@@ -735,6 +735,13 @@ function generatePackageJson(choices) {
|
|
|
735
735
|
// ties this pin to packages/zudo-doc's version, so the lockstep release
|
|
736
736
|
// bumps both together; do not cut a create-zudo-doc release until the
|
|
737
737
|
// matching @takazudo/zudo-doc version (with content.css) is on npm.
|
|
738
|
+
// RELEASE DEPENDENCY (same shape as the content.css one above): the
|
|
739
|
+
// docHistory `dev`/`dev:network` scripts now invoke `run-parallel`, a bin
|
|
740
|
+
// added to @takazudo/zudo-doc in this monorepo but NOT present in published
|
|
741
|
+
// 5.13.1. `^5.13.1` floats up, so a fresh install resolves the bumped
|
|
742
|
+
// version — but do not cut a create-zudo-doc release until a
|
|
743
|
+
// @takazudo/zudo-doc that ships bin/run-parallel.mjs is on npm, or every
|
|
744
|
+
// docHistory scaffold fails at `dev` with `run-parallel: not found`.
|
|
738
745
|
// ZUDO_DOC_PIN is the shared constant — scaffold() uses the same value
|
|
739
746
|
// to seed .zudo-doc.json so the provenance and the dep can never drift.
|
|
740
747
|
"@takazudo/zudo-doc": ZUDO_DOC_PIN,
|
|
@@ -796,9 +803,9 @@ function generatePackageJson(choices) {
|
|
|
796
803
|
// same reason. This is the ACCEPTED, permanent contract per #2668 — see
|
|
797
804
|
// the "@takazudo/zdtp dep implication" note in
|
|
798
805
|
// packages/zudo-doc/docs/adr/route-injection-seam.md.
|
|
799
|
-
"@takazudo/zdtp": "0.4.
|
|
806
|
+
"@takazudo/zdtp": "0.4.14",
|
|
800
807
|
// (@takazudo/zudo-doc-history-server is NOT here — it is gated on the
|
|
801
|
-
// docHistory
|
|
808
|
+
// docHistory or assetViewer features, see the block below. It was briefly unconditional
|
|
802
809
|
// (#3080) to work around doc-history-area importing its `/exclude` subpath
|
|
803
810
|
// at module scope; that root cause was fixed in #3110 by moving
|
|
804
811
|
// compileExclude into @takazudo/zudo-doc itself, so the workaround is gone
|
|
@@ -824,36 +831,31 @@ function generatePackageJson(choices) {
|
|
|
824
831
|
// search ships as @takazudo/zudo-doc's own self-contained generated
|
|
825
832
|
// search-widget script (custom word-match scorer) — no third-party search
|
|
826
833
|
// engine dependency is needed here. Do not re-add minisearch/pagefind.
|
|
827
|
-
if (choices.features.includes("docHistory")
|
|
834
|
+
if (choices.features.includes("docHistory") ||
|
|
835
|
+
choices.features.includes("assetViewer")) {
|
|
828
836
|
// (`diff` remains an unconditional base dep — see the `deps` block above:
|
|
829
837
|
// packageOwnedRoutes always bundles the doc-history-area path, whose
|
|
830
838
|
// module-scope `diff` import is pulled in regardless of this flag. #2342.)
|
|
831
839
|
//
|
|
832
|
-
// @takazudo/zudo-doc-history-server is gated HERE
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
840
|
+
// @takazudo/zudo-doc-history-server is gated HERE because docHistory and
|
|
841
|
+
// assetViewer are the only feature graphs that reach it. The doc-history
|
|
842
|
+
// plugin imports `/git-history` at plugin-init time; the asset viewer loads
|
|
843
|
+
// that same subpath lazily to populate Created/Updated/Author metadata.
|
|
844
|
+
// It is an optional peerDependency of @takazudo/zudo-doc and is not
|
|
845
|
+
// auto-installed for those feature graphs, so either feature must declare
|
|
846
|
+
// it directly. The pin stays lockstep
|
|
839
847
|
// with the root version (parity-guarded — INTERNAL_PINNED_PACKAGES in
|
|
840
848
|
// scripts/check-pin-parity.mjs, and rewritten at release time by
|
|
841
849
|
// scripts/release-create-zudo-doc.sh step 2d).
|
|
842
850
|
//
|
|
843
851
|
// It was briefly unconditional (#3080) because doc-history-area imported
|
|
844
852
|
// `/exclude` at module scope from the always-bundled chrome graph; #3110
|
|
845
|
-
// moved compileExclude into @takazudo/zudo-doc, so
|
|
846
|
-
// no longer need the package at all.
|
|
847
|
-
deps["@takazudo/zudo-doc-history-server"] = "^5.
|
|
853
|
+
// moved compileExclude into @takazudo/zudo-doc, so projects with both
|
|
854
|
+
// docHistory and assetViewer off no longer need the package at all.
|
|
855
|
+
deps["@takazudo/zudo-doc-history-server"] = "^5.14.0";
|
|
848
856
|
// tsx is no longer needed here: the relocated package plugin imports the
|
|
849
857
|
// runner directly (no `tsx -e` spawn) since the package ships compiled
|
|
850
858
|
// dist/ — package-first migration #2321 (#2337).
|
|
851
|
-
// npm-run-all2 provides `run-p`, used by the docHistory `dev` script
|
|
852
|
-
// (below) to run the zfb dev server and the doc-history API server
|
|
853
|
-
// concurrently — otherwise the :4322 proxy target never starts and the
|
|
854
|
-
// feature silently looks broken (#2926). Same maintained fork/pin this
|
|
855
|
-
// monorepo's own root package.json uses.
|
|
856
|
-
devDeps["npm-run-all2"] = "^7.0.2";
|
|
857
859
|
}
|
|
858
860
|
// claudeResources: tsx is no longer needed. The relocated package plugin
|
|
859
861
|
// (@takazudo/zudo-doc/plugins/claude-resources) imports the runner directly
|
|
@@ -879,20 +881,25 @@ function generatePackageJson(choices) {
|
|
|
879
881
|
// running concurrently — otherwise the Created/Updated/Author block
|
|
880
882
|
// silently never appears in dev (#2926). `doc-history-server` is the bin
|
|
881
883
|
// shipped by the @takazudo/zudo-doc-history-server dep added above;
|
|
882
|
-
// `run-
|
|
883
|
-
|
|
884
|
+
// `run-parallel` runs both. It is a bin shipped by @takazudo/zudo-doc — an
|
|
885
|
+
// unconditional dep added above — so concurrency costs this project no extra
|
|
886
|
+
// devDependency. It replaced npm-run-all2's `run-p`, which was the sole
|
|
887
|
+
// source of four advisories (shell-quote DoS; brace-expansion@2 DoS x3).
|
|
888
|
+
scripts.dev = "run-parallel dev:zfb dev:history";
|
|
884
889
|
scripts["dev:zfb"] = "zfb dev";
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
890
|
+
// Neither runner forwards trailing args, but they differ in how loudly: run-p
|
|
891
|
+
// SILENTLY ignored `pnpm dev -- --host 0.0.0.0` (verified in issue #2940),
|
|
892
|
+
// whereas run-parallel rejects it with an error naming the remedy — silence
|
|
893
|
+
// about a flag the user clearly meant is the "quiet lie" #3129 argues against.
|
|
894
|
+
// Either way dev:network is the LAN-bound script
|
|
888
895
|
// instead. Only zfb binds 0.0.0.0; the history server stays loopback-only
|
|
889
896
|
// and LAN clients reach it through zfb's `/doc-history/*` dev proxy.
|
|
890
897
|
scripts["dev:zfb:network"] = "zfb dev --host 0.0.0.0";
|
|
891
|
-
scripts["dev:network"] = "run-
|
|
898
|
+
scripts["dev:network"] = "run-parallel dev:zfb:network dev:history";
|
|
892
899
|
// Relative --content-dir/--locale paths are resolved by resolveContentPath
|
|
893
900
|
// (packages/doc-history-server/src/args.ts) against INIT_CWD (falling back
|
|
894
901
|
// to process.cwd()) — correct for the supported invocation (`<pm> dev` /
|
|
895
|
-
// `<pm> run dev` from the project root, which is what run-
|
|
902
|
+
// `<pm> run dev` from the project root, which is what run-parallel's child
|
|
896
903
|
// processes inherit). It resolves against the WRONG directory only if this
|
|
897
904
|
// generated project is itself nested inside a larger pnpm/npm workspace
|
|
898
905
|
// and dev:history is invoked via `<pm> --filter <this-package> ...` from
|
package/dist/zfb-config-gen.js
CHANGED
|
@@ -62,6 +62,11 @@ export const DEFAULT_MIRROR = {
|
|
|
62
62
|
dynamicPageTransition: false,
|
|
63
63
|
docHistory: false,
|
|
64
64
|
docHistoryExclude: [],
|
|
65
|
+
assetViewer: false,
|
|
66
|
+
assetViewerDir: "assets",
|
|
67
|
+
assetViewerRoutePrefix: "files",
|
|
68
|
+
assetViewerExclude: [],
|
|
69
|
+
assetViewerIndex: false,
|
|
65
70
|
bodyFootUtilArea: false,
|
|
66
71
|
versions: false,
|
|
67
72
|
claudeResources: false,
|
|
@@ -230,6 +235,7 @@ function buildDesiredConfig(choices) {
|
|
|
230
235
|
desired.findInPage = choices.features.includes("tauri");
|
|
231
236
|
desired.dynamicPageTransition = choices.features.includes("dynamicPageTransition");
|
|
232
237
|
desired.docHistory = choices.features.includes("docHistory");
|
|
238
|
+
desired.assetViewer = choices.features.includes("assetViewer");
|
|
233
239
|
if (choices.features.includes("bodyFootUtil")) {
|
|
234
240
|
desired.bodyFootUtilArea = {
|
|
235
241
|
docHistory: choices.features.includes("docHistory"),
|
|
@@ -391,6 +397,11 @@ const FIELD_ORDER = [
|
|
|
391
397
|
"findInPage",
|
|
392
398
|
"dynamicPageTransition",
|
|
393
399
|
"docHistory",
|
|
400
|
+
"assetViewer",
|
|
401
|
+
"assetViewerDir",
|
|
402
|
+
"assetViewerRoutePrefix",
|
|
403
|
+
"assetViewerExclude",
|
|
404
|
+
"assetViewerIndex",
|
|
394
405
|
"bodyFootUtilArea",
|
|
395
406
|
"versions",
|
|
396
407
|
"claudeResources",
|
package/package.json
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/** @jsxRuntime automatic */
|
|
2
2
|
/** @jsxImportSource preact */
|
|
3
|
-
// Locked manifest (#2653 Decision 4): a SELF-CONTAINED doc-route stub
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// distinct from the `/`-injection gap zfb#1227; empirically confirmed on
|
|
7
|
-
// #2653). This stub reconstructs the doc route from scratch using ONLY the
|
|
3
|
+
// Locked manifest (#2653 Decision 4): a SELF-CONTAINED doc-route stub retained
|
|
4
|
+
// for explicit host route ownership. zfb 2.13.1 also serves injected dynamic
|
|
5
|
+
// routes in dev. This stub reconstructs the doc route from scratch using ONLY the
|
|
8
6
|
// sanctioned package entrypoints — no `pages/lib`, no `@/config`:
|
|
9
7
|
// 1. the `virtual:zudo-doc-route-context` virtual module (serializable
|
|
10
8
|
// settings/translations/tagVocabulary/colorSchemes payload),
|
|
@@ -14,9 +12,9 @@
|
|
|
14
12
|
// The bindings import is unconditional: the routes plugin supplies an empty
|
|
15
13
|
// object when `chromeBindingsModule` is unset, while configured projects get
|
|
16
14
|
// their MDX/chrome bindings without editing this stub.
|
|
17
|
-
// Makes `/docs/getting-started/` return 200 in BOTH `zfb dev` and `zfb build
|
|
18
|
-
//
|
|
19
|
-
//
|
|
15
|
+
// Makes `/docs/getting-started/` return 200 in BOTH `zfb dev` and `zfb build`.
|
|
16
|
+
// The route-injection build test covers the corresponding package-owned route;
|
|
17
|
+
// this kept file remains the explicit host-owned seam.
|
|
20
18
|
//
|
|
21
19
|
// docHistory note: when the docHistory feature is selected, the generator
|
|
22
20
|
// patches this file to statically import DocHistory from
|
|
@@ -425,14 +425,34 @@ export async function parseContentDirs(configPath) {
|
|
|
425
425
|
// Shared link and anchor logic
|
|
426
426
|
// ---------------------------------------------------------------------------
|
|
427
427
|
|
|
428
|
+
const HTML_NAMED_CHARACTER_REFERENCES = {
|
|
429
|
+
amp: "&",
|
|
430
|
+
apos: "'",
|
|
431
|
+
gt: ">",
|
|
432
|
+
lt: "<",
|
|
433
|
+
quot: '"',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
function decodeHtmlAttributeValue(value) {
|
|
437
|
+
return value.replace(
|
|
438
|
+
/&(?:#([0-9]+)|#x([0-9a-f]+)|(amp|apos|gt|lt|quot));/gi,
|
|
439
|
+
(_reference, decimal, hexadecimal, named) => {
|
|
440
|
+
if (named !== undefined) return HTML_NAMED_CHARACTER_REFERENCES[named.toLowerCase()];
|
|
441
|
+
const codePoint = Number.parseInt(hexadecimal ?? decimal, hexadecimal === undefined ? 10 : 16);
|
|
442
|
+
if (codePoint === 0 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) return "\uFFFD";
|
|
443
|
+
return String.fromCodePoint(codePoint);
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
428
448
|
export function extractHtmlLinks(html) {
|
|
429
449
|
const links = [];
|
|
430
|
-
const regex = /<a
|
|
450
|
+
const regex = /<a(?=\s)[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`\\]+))[^>]*>/gi;
|
|
431
451
|
let match;
|
|
432
452
|
let lastIndex = 0;
|
|
433
453
|
let line = 1;
|
|
434
454
|
while ((match = regex.exec(html)) !== null) {
|
|
435
|
-
const href = match[1] ?? match[2];
|
|
455
|
+
const href = decodeHtmlAttributeValue(match[1] ?? match[2] ?? match[3]);
|
|
436
456
|
if (/^(?:https?:|mailto:|javascript:|data:|tel:)/i.test(href)) continue;
|
|
437
457
|
for (let i = lastIndex; i < match.index; i += 1) if (html[i] === "\n") line += 1;
|
|
438
458
|
lastIndex = match.index;
|
|
@@ -441,6 +461,16 @@ export function extractHtmlLinks(html) {
|
|
|
441
461
|
return links;
|
|
442
462
|
}
|
|
443
463
|
|
|
464
|
+
export function extractHtmlIds(html) {
|
|
465
|
+
const ids = [];
|
|
466
|
+
const regex = /<[A-Za-z][^>]*?\sid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`\\]+))[^>]*>/gi;
|
|
467
|
+
let match;
|
|
468
|
+
while ((match = regex.exec(html)) !== null) {
|
|
469
|
+
ids.push(decodeHtmlAttributeValue(match[1] ?? match[2] ?? match[3]));
|
|
470
|
+
}
|
|
471
|
+
return ids;
|
|
472
|
+
}
|
|
473
|
+
|
|
444
474
|
function safeDecodePath(path) {
|
|
445
475
|
try {
|
|
446
476
|
return decodeURIComponent(path);
|
|
@@ -455,12 +485,12 @@ function parseHref(href) {
|
|
|
455
485
|
const queryAt = beforeFragment.indexOf("?");
|
|
456
486
|
const rawPath = queryAt === -1 ? beforeFragment : beforeFragment.slice(0, queryAt);
|
|
457
487
|
const rawFragment = hashAt === -1 ? null : href.slice(hashAt + 1);
|
|
458
|
-
if (rawFragment === null) return { path: safeDecodePath(rawPath), fragment: null, fragmentError: null };
|
|
459
|
-
if (rawFragment === "") return { path: safeDecodePath(rawPath), fragment: "", fragmentError: "empty fragment" };
|
|
488
|
+
if (rawFragment === null) return { path: safeDecodePath(rawPath), rawPath, fragment: null, fragmentError: null };
|
|
489
|
+
if (rawFragment === "") return { path: safeDecodePath(rawPath), rawPath, fragment: "", fragmentError: "empty fragment" };
|
|
460
490
|
try {
|
|
461
|
-
return { path: safeDecodePath(rawPath), fragment: decodeURIComponent(rawFragment), fragmentError: null };
|
|
491
|
+
return { path: safeDecodePath(rawPath), rawPath, fragment: decodeURIComponent(rawFragment), fragmentError: null };
|
|
462
492
|
} catch {
|
|
463
|
-
return { path: safeDecodePath(rawPath), fragment: rawFragment, fragmentError: "malformed percent-encoding" };
|
|
493
|
+
return { path: safeDecodePath(rawPath), rawPath, fragment: rawFragment, fragmentError: "malformed percent-encoding" };
|
|
464
494
|
}
|
|
465
495
|
}
|
|
466
496
|
|
|
@@ -646,23 +676,40 @@ export async function checkMdxAnchors(contentDirs, rootDir, basePath = "/", loca
|
|
|
646
676
|
return anchors;
|
|
647
677
|
}
|
|
648
678
|
|
|
649
|
-
async function
|
|
650
|
-
|
|
651
|
-
if (!
|
|
652
|
-
let absolute = clean;
|
|
653
|
-
if (!clean.startsWith("/")) absolute = "/" + join(fileDir ? relative(distDir, fileDir) : "", clean);
|
|
679
|
+
async function resolveBuiltPath(path, distDir, basePath, fileDir) {
|
|
680
|
+
let absolute = path;
|
|
681
|
+
if (!path.startsWith("/")) absolute = "/" + join(fileDir ? relative(distDir, fileDir) : "", path);
|
|
654
682
|
let stripped = absolute;
|
|
655
683
|
if (basePath !== "/" && stripped.startsWith(basePath)) stripped = "/" + stripped.slice(basePath.length);
|
|
656
684
|
const relPath = stripped.startsWith("/") ? stripped.slice(1) : stripped;
|
|
657
|
-
if (!relPath) return { type: "root", targetFile: join(distDir, "index.html")
|
|
685
|
+
if (!relPath) return { type: "root", targetFile: join(distDir, "index.html") };
|
|
686
|
+
// A terminal slash is an explicit directory request, even when the
|
|
687
|
+
// directory name contains a dot (for example, /files/demo/x.js/). Check it
|
|
688
|
+
// before extname() so viewer pages can keep their trailing-slash route and
|
|
689
|
+
// still receive fragment validation against index.html.
|
|
690
|
+
if (relPath.endsWith("/")) {
|
|
691
|
+
const indexFile = join(distDir, relPath, "index.html");
|
|
692
|
+
return (await fileExists(indexFile)) ? { type: "directoryIndex", targetFile: indexFile } : { type: "missing", targetFile: null };
|
|
693
|
+
}
|
|
658
694
|
if (extname(relPath)) {
|
|
659
695
|
const targetFile = join(distDir, relPath);
|
|
660
|
-
return
|
|
696
|
+
return (await fileExists(targetFile)) ? { type: "file", targetFile } : { type: "missing", targetFile: null };
|
|
661
697
|
}
|
|
662
698
|
const indexFile = join(distDir, relPath, "index.html");
|
|
663
|
-
if (await fileExists(indexFile)) return { type: "directoryIndex", targetFile: indexFile
|
|
699
|
+
if (await fileExists(indexFile)) return { type: "directoryIndex", targetFile: indexFile };
|
|
664
700
|
const htmlFile = join(distDir, relPath + ".html");
|
|
665
|
-
if (await fileExists(htmlFile)) return { type: "file", targetFile: htmlFile
|
|
701
|
+
if (await fileExists(htmlFile)) return { type: "file", targetFile: htmlFile };
|
|
702
|
+
return { type: "missing", targetFile: null };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function resolveDistTarget(href, distDir, basePath = "/", fileDir = "", sourceFile = null) {
|
|
706
|
+
const { path: decodedPath, rawPath, fragment, fragmentError } = parseHref(href);
|
|
707
|
+
if (!rawPath) return { type: "root", targetFile: sourceFile ?? join(distDir, "index.html"), fragment, fragmentError };
|
|
708
|
+
const pathCandidates = rawPath === decodedPath ? [rawPath] : [rawPath, decodedPath];
|
|
709
|
+
for (const path of pathCandidates) {
|
|
710
|
+
const detail = await resolveBuiltPath(path, distDir, basePath, fileDir);
|
|
711
|
+
if (detail.type !== "missing") return { ...detail, fragment, fragmentError };
|
|
712
|
+
}
|
|
666
713
|
return { type: "missing", targetFile: null, fragment, fragmentError };
|
|
667
714
|
}
|
|
668
715
|
|
|
@@ -686,9 +733,19 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
686
733
|
const trailingSlash = [];
|
|
687
734
|
const idCache = new Map();
|
|
688
735
|
const cache = new Map();
|
|
736
|
+
const pages = [];
|
|
737
|
+
const scanned = { links: 0, ids: 0 };
|
|
689
738
|
for (const file of await collectFiles(distDir, [".html"])) {
|
|
690
739
|
const content = await readFile(file, "utf-8");
|
|
691
|
-
|
|
740
|
+
const links = extractHtmlLinks(content);
|
|
741
|
+
const ids = extractHtmlIds(content);
|
|
742
|
+
scanned.links += links.length;
|
|
743
|
+
scanned.ids += ids.length;
|
|
744
|
+
idCache.set(file, new Set(ids));
|
|
745
|
+
pages.push({ file, links });
|
|
746
|
+
}
|
|
747
|
+
for (const { file, links } of pages) {
|
|
748
|
+
for (const { href, line } of links) {
|
|
692
749
|
if (excludePatterns.some((pattern) => pattern.test(href))) continue;
|
|
693
750
|
const cacheKey = href.startsWith("/") ? href : `${file}:${href}`;
|
|
694
751
|
let detail = cache.get(cacheKey);
|
|
@@ -703,10 +760,9 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
703
760
|
let ids = idCache.get(detail.targetFile);
|
|
704
761
|
if (ids === undefined) {
|
|
705
762
|
const targetHtml = await readFile(detail.targetFile, "utf-8");
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
while ((idMatch = idRegex.exec(targetHtml)) !== null) ids.add(idMatch[1] ?? idMatch[2]);
|
|
763
|
+
const targetIds = extractHtmlIds(targetHtml);
|
|
764
|
+
scanned.ids += targetIds.length;
|
|
765
|
+
ids = new Set(targetIds);
|
|
710
766
|
idCache.set(detail.targetFile, ids);
|
|
711
767
|
}
|
|
712
768
|
if (!ids.has(detail.fragment)) reason = "missing target id";
|
|
@@ -721,7 +777,7 @@ export async function checkHtmlLinksAndTrailing(
|
|
|
721
777
|
}
|
|
722
778
|
}
|
|
723
779
|
}
|
|
724
|
-
return { broken, anchors, trailingSlash };
|
|
780
|
+
return { broken, anchors, trailingSlash, scanned };
|
|
725
781
|
}
|
|
726
782
|
|
|
727
783
|
export async function checkMdxLinks(
|
|
@@ -796,8 +852,8 @@ async function main() {
|
|
|
796
852
|
console.log(`Checking links (base: ${config.basePath}, trailingSlash: ${config.trailingSlash})...`);
|
|
797
853
|
console.log(`Source scan: ${contentDirs.map((dir) => relative(rootDir, dir) || ".").join(", ")}${hasDist ? "; dist/ pass enabled" : "; dist/ absent (source-only)"}\n`);
|
|
798
854
|
|
|
799
|
-
const [{ broken, anchors: htmlAnchors, trailingSlash }, mdxWarnings, mdxAnchors] = await Promise.all([
|
|
800
|
-
hasDist ? checkHtmlLinksAndTrailing(distDir, rootDir, config.basePath, excludePatterns, config.trailingSlash) : Promise.resolve({ broken: [], anchors: [], trailingSlash: [] }),
|
|
855
|
+
const [{ broken, anchors: htmlAnchors, trailingSlash, scanned }, mdxWarnings, mdxAnchors] = await Promise.all([
|
|
856
|
+
hasDist ? checkHtmlLinksAndTrailing(distDir, rootDir, config.basePath, excludePatterns, config.trailingSlash) : Promise.resolve({ broken: [], anchors: [], trailingSlash: [], scanned: { links: 0, ids: 0 } }),
|
|
801
857
|
checkMdxLinks(contentDirs, rootDir, hasDist ? distDir : null, config.basePath, config.localeKeys),
|
|
802
858
|
checkMdxAnchors(contentDirs, rootDir, config.basePath, config.localeKeys, excludePatterns),
|
|
803
859
|
]);
|
|
@@ -810,6 +866,7 @@ async function main() {
|
|
|
810
866
|
const realAnchors = filter(anchorWarnings);
|
|
811
867
|
const realTrailing = filter(trailingSlash);
|
|
812
868
|
console.log(formatReport(broken, mdxWarnings, trailingSlash, anchorWarnings));
|
|
869
|
+
if (hasDist) console.log(`\nBuilt HTML scan: ${scanned.links} internal link${scanned.links === 1 ? "" : "s"} and ${scanned.ids} ID attribute${scanned.ids === 1 ? "" : "s"} inspected.`);
|
|
813
870
|
const skipped = broken.length - realBroken.length + mdxWarnings.length - realAbsolute.length + anchorWarnings.length - realAnchors.length + trailingSlash.length - realTrailing.length;
|
|
814
871
|
if (skipped > 0) console.log(`\nAllowlist: ${skipped} known exception${skipped === 1 ? "" : "s"} excluded from strict-mode counts (${allowlistPath}).`);
|
|
815
872
|
let failed = false;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** @jsxRuntime automatic */
|
|
2
2
|
/** @jsxImportSource preact */
|
|
3
3
|
// Locked manifest (#2653 Decision 4, i18n addendum): the locale-prefixed
|
|
4
|
-
// counterpart of pages/docs/[[...slug]].tsx
|
|
5
|
-
//
|
|
4
|
+
// counterpart of pages/docs/[[...slug]].tsx, retained for explicit host route
|
|
5
|
+
// ownership; zfb 2.13.1 also serves injected dynamic routes in dev. Self-contained: only the
|
|
6
6
|
// sanctioned package entrypoints — no `pages/lib`, no `@/config`. The
|
|
7
7
|
// `virtual:zudo-doc-chrome-bindings` import is unconditional, just like the
|
|
8
8
|
// default-locale stub: the routes plugin supplies `{}` when no host module is
|