create-zudo-doc 0.2.20 → 0.2.22
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/dist/features/image-enlarge.d.ts +10 -2
- package/dist/features/image-enlarge.js +10 -2
- package/dist/scaffold.js +22 -22
- package/dist/zfb-config-gen.d.ts +16 -10
- package/dist/zfb-config-gen.js +34 -239
- package/package.json +1 -1
- package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
- package/templates/base/pages/lib/_category-nav.tsx +30 -115
- package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
- package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
- package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
- package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
- package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
- package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
- package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
- package/templates/base/pages/lib/_doc-pager.tsx +4 -74
- package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
- package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
- package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
- package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
- package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
- package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
- package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
- package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
- package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
- package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
- package/templates/base/pages/lib/_search-widget-script.ts +2 -470
- package/templates/base/pages/lib/_search-widget.tsx +9 -195
- package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -47
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
- package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
- package/templates/base/pages/lib/doc-page-props.ts +26 -44
- package/templates/base/pages/lib/locale-merge.ts +32 -145
- package/templates/base/pages/lib/route-enumerators.ts +52 -286
- package/templates/base/pages/robots.txt.tsx +2 -26
- package/templates/base/src/components/ai-chat-modal.tsx +9 -8
- package/templates/base/src/components/doc-history.tsx +9 -8
- package/templates/base/src/components/image-enlarge.tsx +11 -8
- package/templates/base/src/components/sidebar-toggle.tsx +6 -170
- package/templates/base/src/components/sidebar-tree.tsx +6 -548
- package/templates/base/src/components/site-tree-nav.tsx +6 -220
- package/templates/base/src/config/color-scheme-utils.ts +34 -158
- package/templates/base/src/config/i18n.ts +9 -0
- package/templates/base/src/config/z-index-tokens.ts +5 -4
- package/templates/base/src/styles/global.css +5 -579
- package/templates/base/src/utils/base.ts +1 -1
- package/templates/base/src/utils/docs.ts +47 -16
- package/templates/base/src/utils/github.ts +12 -9
- package/templates/base/src/utils/nav-scope.ts +13 -42
- package/templates/base/src/utils/sidebar.ts +18 -86
- package/templates/base/src/utils/slug.ts +10 -53
- package/templates/base/src/utils/smart-break.tsx +12 -120
- package/templates/base/src/utils/tags.ts +25 -68
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
- package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
- package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +1 -4
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +1 -1
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +1 -1
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
- package/templates/base/plugins/connect-adapter.mjs +0 -169
- package/templates/base/plugins/search-index-plugin.mjs +0 -66
- package/templates/base/scripts/gen-z-index.mjs +0 -157
- package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
|
@@ -1,534 +1,93 @@
|
|
|
1
1
|
#!/usr/bin/env -S tsx
|
|
2
2
|
/**
|
|
3
|
-
* tags-audit.ts
|
|
3
|
+
* scripts/tags-audit.ts
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - near-duplicates (string-similarity or shared singular form)
|
|
9
|
-
* - orphan vocab (vocab id referenced by nothing)
|
|
5
|
+
* Thin re-export shim — the audit logic lives in @takazudo/zudo-doc/tags-audit
|
|
6
|
+
* (S9b #2334). This file exists for backward-compat with any project-side
|
|
7
|
+
* imports of these functions (e.g. custom scripts that import helpers from here).
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* The `tags:audit` package.json script does not invoke this file directly;
|
|
10
|
+
* it uses the `tags-audit` package bin from @takazudo/zudo-doc instead.
|
|
11
|
+
*
|
|
12
|
+
* CLI usage (still works via tsx for debugging):
|
|
13
|
+
* tsx scripts/tags-audit.ts [--fix] [--ci] [--json]
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
import {
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
21
17
|
import { fileURLToPath } from "node:url";
|
|
22
18
|
|
|
23
|
-
import matter from "gray-matter";
|
|
24
19
|
import pc from "picocolors";
|
|
25
20
|
import pluralize from "pluralize";
|
|
26
21
|
import stringSimilarity from "string-similarity";
|
|
27
22
|
|
|
28
23
|
import { settings } from "../src/config/settings";
|
|
29
24
|
import { tagVocabulary } from "../src/config/tag-vocabulary";
|
|
30
|
-
import type { TagVocabularyEntry } from "../src/config/tag-vocabulary-types";
|
|
31
|
-
|
|
32
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
-
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
34
|
-
const ROOT_DIR = resolve(__dirname, "..");
|
|
35
|
-
|
|
36
|
-
/** Similarity threshold above which two distinct tags are flagged as near-duplicates. */
|
|
37
|
-
const NEAR_DUP_THRESHOLD = 0.82;
|
|
38
|
-
|
|
39
|
-
// ── Types ──────────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
interface VocabularyIndex {
|
|
42
|
-
byId: Map<string, TagVocabularyEntry>;
|
|
43
|
-
byAlias: Map<string, TagVocabularyEntry>;
|
|
44
|
-
aliasToId: Map<string, string>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface FileTagRef {
|
|
48
|
-
/** File path relative to repo root. */
|
|
49
|
-
file: string;
|
|
50
|
-
/** Raw tag as written in frontmatter. */
|
|
51
|
-
raw: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface UnknownIssue {
|
|
55
|
-
file: string;
|
|
56
|
-
raw: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface DeprecatedIssue {
|
|
60
|
-
file: string;
|
|
61
|
-
raw: string;
|
|
62
|
-
canonical: string;
|
|
63
|
-
redirect?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface AliasIssue {
|
|
67
|
-
file: string;
|
|
68
|
-
raw: string;
|
|
69
|
-
canonical: string;
|
|
70
|
-
}
|
|
71
25
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
byId.set(entry.id, entry);
|
|
106
|
-
for (const alias of entry.aliases ?? []) {
|
|
107
|
-
byAlias.set(alias, entry);
|
|
108
|
-
aliasToId.set(alias, entry.id);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { byId, byAlias, aliasToId };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── File walking ───────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
async function collectMdxFiles(dir: string): Promise<string[]> {
|
|
117
|
-
const out: string[] = [];
|
|
118
|
-
async function walk(current: string) {
|
|
119
|
-
let entries;
|
|
120
|
-
try {
|
|
121
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
122
|
-
} catch {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
for (const entry of entries) {
|
|
126
|
-
const full = join(current, entry.name);
|
|
127
|
-
if (entry.isDirectory()) {
|
|
128
|
-
await walk(full);
|
|
129
|
-
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
|
|
130
|
-
out.push(full);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
await walk(dir);
|
|
135
|
-
return out.sort();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── Core audit ─────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
function normalizeTags(value: unknown): string[] {
|
|
141
|
-
if (!Array.isArray(value)) return [];
|
|
142
|
-
return value.filter((v): v is string => typeof v === "string");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function audit(opts: AuditOptions): Promise<AuditReport> {
|
|
146
|
-
const index = buildIndex(opts.vocabulary);
|
|
147
|
-
|
|
148
|
-
const unknowns: UnknownIssue[] = [];
|
|
149
|
-
const deprecated: DeprecatedIssue[] = [];
|
|
150
|
-
const aliases: AliasIssue[] = [];
|
|
151
|
-
const frequency: Record<string, number> = {};
|
|
152
|
-
const canonicalsUsed = new Set<string>();
|
|
153
|
-
const rawTagsUsed = new Set<string>();
|
|
154
|
-
let filesScanned = 0;
|
|
155
|
-
|
|
156
|
-
for (const dir of opts.contentDirs) {
|
|
157
|
-
if (!existsSync(dir)) continue;
|
|
158
|
-
const files = await collectMdxFiles(dir);
|
|
159
|
-
for (const file of files) {
|
|
160
|
-
filesScanned++;
|
|
161
|
-
const raw = await readFile(file, "utf-8");
|
|
162
|
-
const parsed = matter(raw);
|
|
163
|
-
const tags = normalizeTags(parsed.data.tags);
|
|
164
|
-
const rel = relative(opts.rootDir, file);
|
|
165
|
-
|
|
166
|
-
for (const tag of tags) {
|
|
167
|
-
rawTagsUsed.add(tag);
|
|
168
|
-
const idEntry = index.byId.get(tag);
|
|
169
|
-
const aliasEntry = index.byAlias.get(tag);
|
|
170
|
-
|
|
171
|
-
if (!opts.vocabularyActive) {
|
|
172
|
-
frequency[tag] = (frequency[tag] ?? 0) + 1;
|
|
173
|
-
canonicalsUsed.add(tag);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (!idEntry && !aliasEntry) {
|
|
178
|
-
unknowns.push({ file: rel, raw: tag });
|
|
179
|
-
frequency[tag] = (frequency[tag] ?? 0) + 1;
|
|
180
|
-
canonicalsUsed.add(tag);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const entry = idEntry ?? aliasEntry!;
|
|
185
|
-
let canonical = entry.id;
|
|
186
|
-
|
|
187
|
-
const dep = entry.deprecated;
|
|
188
|
-
if (dep) {
|
|
189
|
-
if (typeof dep === "object" && dep.redirect) {
|
|
190
|
-
const target = index.byId.get(dep.redirect);
|
|
191
|
-
if (target) {
|
|
192
|
-
deprecated.push({ file: rel, raw: tag, canonical: entry.id, redirect: target.id });
|
|
193
|
-
canonical = target.id;
|
|
194
|
-
} else {
|
|
195
|
-
deprecated.push({ file: rel, raw: tag, canonical: entry.id });
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
deprecated.push({ file: rel, raw: tag, canonical: entry.id });
|
|
199
|
-
}
|
|
200
|
-
} else if (aliasEntry && tag !== entry.id) {
|
|
201
|
-
aliases.push({ file: rel, raw: tag, canonical: entry.id });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
frequency[canonical] = (frequency[canonical] ?? 0) + 1;
|
|
205
|
-
canonicalsUsed.add(canonical);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const nearDuplicates = opts.vocabularyActive
|
|
211
|
-
? []
|
|
212
|
-
: findNearDuplicates(Array.from(rawTagsUsed));
|
|
213
|
-
// When the vocabulary is active, near-duplicate detection runs over the
|
|
214
|
-
// canonical set (post-alias) so we don't re-flag resolved aliases.
|
|
215
|
-
const nearDupSource = opts.vocabularyActive
|
|
216
|
-
? Array.from(canonicalsUsed)
|
|
217
|
-
: Array.from(rawTagsUsed);
|
|
218
|
-
if (opts.vocabularyActive) {
|
|
219
|
-
nearDuplicates.push(...findNearDuplicates(nearDupSource));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const orphans = opts.vocabularyActive
|
|
223
|
-
? opts.vocabulary
|
|
224
|
-
.filter((entry) => {
|
|
225
|
-
if (entry.deprecated) return false;
|
|
226
|
-
if (canonicalsUsed.has(entry.id)) return false;
|
|
227
|
-
for (const alias of entry.aliases ?? []) {
|
|
228
|
-
if (rawTagsUsed.has(alias)) return false;
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
})
|
|
232
|
-
.map((entry) => entry.id)
|
|
233
|
-
: [];
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
unknowns,
|
|
237
|
-
deprecated,
|
|
238
|
-
aliases,
|
|
239
|
-
nearDuplicates,
|
|
240
|
-
orphans,
|
|
241
|
-
filesScanned,
|
|
242
|
-
frequency,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function findNearDuplicates(tags: string[]): NearDuplicatePair[] {
|
|
247
|
-
const pairs: NearDuplicatePair[] = [];
|
|
248
|
-
const seen = new Set<string>();
|
|
249
|
-
const sorted = [...tags].sort();
|
|
250
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
251
|
-
for (let j = i + 1; j < sorted.length; j++) {
|
|
252
|
-
const a = sorted[i]!;
|
|
253
|
-
const b = sorted[j]!;
|
|
254
|
-
const key = `${a}||${b}`;
|
|
255
|
-
if (seen.has(key)) continue;
|
|
256
|
-
seen.add(key);
|
|
257
|
-
|
|
258
|
-
const singA = pluralize.singular(a);
|
|
259
|
-
const singB = pluralize.singular(b);
|
|
260
|
-
if (singA === singB && a !== b) {
|
|
261
|
-
pairs.push({ a, b, reason: "plural" });
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
const score = stringSimilarity.compareTwoStrings(a, b);
|
|
265
|
-
if (score >= NEAR_DUP_THRESHOLD) {
|
|
266
|
-
pairs.push({ a, b, reason: "similarity", score });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return pairs;
|
|
26
|
+
// Re-export everything from the package core so existing callers continue
|
|
27
|
+
// to work unchanged.
|
|
28
|
+
export {
|
|
29
|
+
audit,
|
|
30
|
+
applyFixes,
|
|
31
|
+
buildIndex,
|
|
32
|
+
collectMdxFiles,
|
|
33
|
+
computeRewrites,
|
|
34
|
+
formatTextReport,
|
|
35
|
+
hasHardIssues,
|
|
36
|
+
normalizeTags,
|
|
37
|
+
rewriteAliasesByteStable,
|
|
38
|
+
} from "@takazudo/zudo-doc/tags-audit";
|
|
39
|
+
export type {
|
|
40
|
+
AliasIssue,
|
|
41
|
+
AuditOptions,
|
|
42
|
+
AuditReport,
|
|
43
|
+
DeprecatedIssue,
|
|
44
|
+
NearDuplicatePair,
|
|
45
|
+
NearDupHelpers,
|
|
46
|
+
TagVocabularyEntry,
|
|
47
|
+
UnknownIssue,
|
|
48
|
+
} from "@takazudo/zudo-doc/tags-audit";
|
|
49
|
+
|
|
50
|
+
// Backward-compat: expose findNearDuplicates with project helpers pre-bound.
|
|
51
|
+
import { findNearDuplicates as _findNearDuplicates } from "@takazudo/zudo-doc/tags-audit";
|
|
52
|
+
export function findNearDuplicates(
|
|
53
|
+
tags: string[],
|
|
54
|
+
): import("@takazudo/zudo-doc/tags-audit").NearDuplicatePair[] {
|
|
55
|
+
return _findNearDuplicates(tags, {
|
|
56
|
+
singular: pluralize.singular,
|
|
57
|
+
compareTwoStrings: stringSimilarity.compareTwoStrings,
|
|
58
|
+
});
|
|
271
59
|
}
|
|
272
60
|
|
|
273
|
-
// ──
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Rewrite alias tags to canonical ids within a single MDX file, preserving
|
|
277
|
-
* every other byte of the file verbatim. Returns `{ content, changed }`.
|
|
278
|
-
*
|
|
279
|
-
* Supports block-style (`tags:\n - foo\n`) and flow-style
|
|
280
|
-
* (`tags: [foo, bar]`) YAML sequences. Quoted string values are handled.
|
|
281
|
-
*/
|
|
282
|
-
export function rewriteAliasesByteStable(
|
|
283
|
-
content: string,
|
|
284
|
-
rewrites: Map<string, string>,
|
|
285
|
-
): { content: string; changed: boolean } {
|
|
286
|
-
if (rewrites.size === 0) return { content, changed: false };
|
|
287
|
-
|
|
288
|
-
const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/);
|
|
289
|
-
if (!fmMatch) return { content, changed: false };
|
|
290
|
-
const whole = fmMatch[0] ?? "";
|
|
291
|
-
const open = fmMatch[1] ?? "";
|
|
292
|
-
const fmBody = fmMatch[2] ?? "";
|
|
293
|
-
const close = fmMatch[3] ?? "";
|
|
294
|
-
|
|
295
|
-
let changed = false;
|
|
296
|
-
let newBody = fmBody;
|
|
297
|
-
|
|
298
|
-
// Flow-style: tags: [a, b, "c"]
|
|
299
|
-
newBody = newBody.replace(
|
|
300
|
-
/^(tags[ \t]*:[ \t]*\[)([^\]\n]*)(\])/m,
|
|
301
|
-
(_full, pre, inner: string, post) => {
|
|
302
|
-
const rewritten = inner.replace(
|
|
303
|
-
/(^|,)([ \t]*)(?:(")([^"]*)(")|(')([^']*)(')|([^,\s"'][^,]*?))([ \t]*)(?=,|$)/g,
|
|
304
|
-
(
|
|
305
|
-
_m,
|
|
306
|
-
lead,
|
|
307
|
-
leadWs,
|
|
308
|
-
dq1,
|
|
309
|
-
dqVal,
|
|
310
|
-
dq2,
|
|
311
|
-
sq1,
|
|
312
|
-
sqVal,
|
|
313
|
-
sq2,
|
|
314
|
-
bareVal,
|
|
315
|
-
trailWs,
|
|
316
|
-
) => {
|
|
317
|
-
const val: string = dqVal ?? sqVal ?? bareVal ?? "";
|
|
318
|
-
const target = rewrites.get(val.trim());
|
|
319
|
-
if (!target || target === val) return _m;
|
|
320
|
-
changed = true;
|
|
321
|
-
if (dq1) return `${lead}${leadWs}"${target}"${trailWs}`;
|
|
322
|
-
if (sq1) return `${lead}${leadWs}'${target}'${trailWs}`;
|
|
323
|
-
return `${lead}${leadWs}${target}${trailWs}`;
|
|
324
|
-
},
|
|
325
|
-
);
|
|
326
|
-
return pre + rewritten + post;
|
|
327
|
-
},
|
|
328
|
-
);
|
|
61
|
+
// ── CLI runner (legacy — package bin is preferred) ─────────────────────────
|
|
329
62
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const rewritten = items.replace(
|
|
335
|
-
/^([ \t]+-[ \t]+)(?:(")([^"]*)(")|(')([^']*)('))?([^\n]*)$/gm,
|
|
336
|
-
(line, dash, dq1, dqVal, dq2, sq1, sqVal, sq2, bare) => {
|
|
337
|
-
if (dq1) {
|
|
338
|
-
const target = rewrites.get(dqVal);
|
|
339
|
-
if (target && target !== dqVal) {
|
|
340
|
-
changed = true;
|
|
341
|
-
return `${dash}"${target}"${bare}`;
|
|
342
|
-
}
|
|
343
|
-
return line;
|
|
344
|
-
}
|
|
345
|
-
if (sq1) {
|
|
346
|
-
const target = rewrites.get(sqVal);
|
|
347
|
-
if (target && target !== sqVal) {
|
|
348
|
-
changed = true;
|
|
349
|
-
return `${dash}'${target}'${bare}`;
|
|
350
|
-
}
|
|
351
|
-
return line;
|
|
352
|
-
}
|
|
353
|
-
// Trailing whitespace includes \r for CRLF line endings — strip it
|
|
354
|
-
// before the lookup, preserve it in the emitted line.
|
|
355
|
-
const trimmed = bare.replace(/[ \t\r]+$/, "");
|
|
356
|
-
const trailing = bare.slice(trimmed.length);
|
|
357
|
-
const target = rewrites.get(trimmed);
|
|
358
|
-
if (target && target !== trimmed) {
|
|
359
|
-
changed = true;
|
|
360
|
-
return `${dash}${target}${trailing}`;
|
|
361
|
-
}
|
|
362
|
-
return line;
|
|
363
|
-
},
|
|
364
|
-
);
|
|
365
|
-
return pre + rewritten;
|
|
366
|
-
},
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
if (!changed) return { content, changed: false };
|
|
370
|
-
return {
|
|
371
|
-
content: open + newBody + close + content.slice(whole.length),
|
|
372
|
-
changed: true,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// ── Runner + reporting ─────────────────────────────────────────────────────
|
|
63
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
64
|
+
const isMain =
|
|
65
|
+
process.argv[1] !== undefined &&
|
|
66
|
+
resolve(process.argv[1]) === resolve(__filename);
|
|
377
67
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
json: boolean;
|
|
382
|
-
}
|
|
68
|
+
if (isMain) {
|
|
69
|
+
const { audit: coreAudit, computeRewrites: coreComputeRewrites, applyFixes: coreApplyFixes, hasHardIssues: coreHasHardIssues, formatTextReport: coreFormatReport } =
|
|
70
|
+
await import("@takazudo/zudo-doc/tags-audit");
|
|
383
71
|
|
|
384
|
-
|
|
385
|
-
|
|
72
|
+
const argv = process.argv.slice(2);
|
|
73
|
+
const flags = {
|
|
386
74
|
fix: argv.includes("--fix"),
|
|
387
75
|
ci: argv.includes("--ci"),
|
|
388
76
|
json: argv.includes("--json"),
|
|
389
77
|
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function formatTextReport(
|
|
393
|
-
report: AuditReport,
|
|
394
|
-
governance: AuditOptions["governance"],
|
|
395
|
-
vocabularyActive: boolean,
|
|
396
|
-
): string {
|
|
397
|
-
const lines: string[] = [];
|
|
398
|
-
lines.push(pc.bold(pc.cyan(`tags:audit — scanned ${report.filesScanned} file(s)`)));
|
|
399
|
-
lines.push(
|
|
400
|
-
pc.dim(
|
|
401
|
-
`vocabulary: ${vocabularyActive ? "active" : "inactive"}, governance: ${governance}`,
|
|
402
|
-
),
|
|
403
|
-
);
|
|
404
|
-
lines.push("");
|
|
405
78
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
for (const { file, raw } of report.unknowns) {
|
|
409
|
-
lines.push(` ${pc.red("•")} ${file} ${pc.yellow(raw)}`);
|
|
410
|
-
}
|
|
411
|
-
lines.push("");
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (report.deprecated.length > 0) {
|
|
415
|
-
lines.push(pc.bold(pc.yellow(`⚠ Deprecated tags (${report.deprecated.length})`)));
|
|
416
|
-
for (const { file, raw, redirect } of report.deprecated) {
|
|
417
|
-
const suffix = redirect ? pc.dim(` → ${redirect}`) : pc.dim(" (dropped)");
|
|
418
|
-
lines.push(` ${pc.yellow("•")} ${file} ${raw}${suffix}`);
|
|
419
|
-
}
|
|
420
|
-
lines.push("");
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (report.aliases.length > 0) {
|
|
424
|
-
lines.push(pc.bold(pc.blue(`ℹ Alias usage (${report.aliases.length})`)));
|
|
425
|
-
for (const { file, raw, canonical } of report.aliases) {
|
|
426
|
-
lines.push(` ${pc.blue("•")} ${file} ${raw} ${pc.dim(`→ ${canonical}`)}`);
|
|
427
|
-
}
|
|
428
|
-
lines.push(pc.dim(` (run with --fix to rewrite these in place)`));
|
|
429
|
-
lines.push("");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (report.nearDuplicates.length > 0) {
|
|
433
|
-
lines.push(pc.bold(pc.yellow(`⚠ Near-duplicate tags (${report.nearDuplicates.length})`)));
|
|
434
|
-
for (const pair of report.nearDuplicates) {
|
|
435
|
-
const hint =
|
|
436
|
-
pair.reason === "plural"
|
|
437
|
-
? "same singular"
|
|
438
|
-
: `similarity ${(pair.score ?? 0).toFixed(2)}`;
|
|
439
|
-
lines.push(` ${pc.yellow("•")} ${pair.a} ↔ ${pair.b} ${pc.dim(`(${hint})`)}`);
|
|
440
|
-
}
|
|
441
|
-
lines.push("");
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (report.orphans.length > 0) {
|
|
445
|
-
lines.push(pc.bold(pc.dim(`… Orphan vocabulary entries (${report.orphans.length})`)));
|
|
446
|
-
for (const id of report.orphans) {
|
|
447
|
-
lines.push(` ${pc.dim("•")} ${id}`);
|
|
448
|
-
}
|
|
449
|
-
lines.push("");
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
report.unknowns.length === 0 &&
|
|
454
|
-
report.deprecated.length === 0 &&
|
|
455
|
-
report.aliases.length === 0 &&
|
|
456
|
-
report.nearDuplicates.length === 0 &&
|
|
457
|
-
report.orphans.length === 0
|
|
458
|
-
) {
|
|
459
|
-
lines.push(pc.green("✓ No tag issues found"));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return lines.join("\n");
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export function hasHardIssues(report: AuditReport): boolean {
|
|
466
|
-
return report.unknowns.length > 0 || report.deprecated.length > 0;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
export function computeRewrites(
|
|
470
|
-
vocabulary: readonly TagVocabularyEntry[],
|
|
471
|
-
): Map<string, string> {
|
|
472
|
-
const rewrites = new Map<string, string>();
|
|
473
|
-
const { byId } = buildIndex(vocabulary);
|
|
474
|
-
for (const entry of vocabulary) {
|
|
475
|
-
for (const alias of entry.aliases ?? []) {
|
|
476
|
-
if (alias === entry.id) continue;
|
|
477
|
-
const dep = entry.deprecated;
|
|
478
|
-
if (dep && typeof dep === "object" && dep.redirect) {
|
|
479
|
-
const target = byId.get(dep.redirect);
|
|
480
|
-
rewrites.set(alias, target ? target.id : entry.id);
|
|
481
|
-
} else {
|
|
482
|
-
rewrites.set(alias, entry.id);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// A deprecated entry with a redirect: rewrite the id itself.
|
|
486
|
-
const dep = entry.deprecated;
|
|
487
|
-
if (dep && typeof dep === "object" && dep.redirect) {
|
|
488
|
-
const target = byId.get(dep.redirect);
|
|
489
|
-
if (target) rewrites.set(entry.id, target.id);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return rewrites;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
async function applyFixes(
|
|
496
|
-
contentDirs: string[],
|
|
497
|
-
rewrites: Map<string, string>,
|
|
498
|
-
rootDir: string,
|
|
499
|
-
): Promise<string[]> {
|
|
500
|
-
const touched: string[] = [];
|
|
501
|
-
for (const dir of contentDirs) {
|
|
502
|
-
if (!existsSync(dir)) continue;
|
|
503
|
-
const files = await collectMdxFiles(dir);
|
|
504
|
-
for (const file of files) {
|
|
505
|
-
const original = await readFile(file, "utf-8");
|
|
506
|
-
const { content, changed } = rewriteAliasesByteStable(original, rewrites);
|
|
507
|
-
if (changed) {
|
|
508
|
-
await writeFile(file, content, "utf-8");
|
|
509
|
-
touched.push(relative(rootDir, file));
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
return touched;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async function main() {
|
|
517
|
-
const flags = parseArgs(process.argv.slice(2));
|
|
518
|
-
const rootDir = ROOT_DIR;
|
|
519
|
-
|
|
520
|
-
const docsDir = join(rootDir, settings.docsDir);
|
|
79
|
+
const ROOT_DIR = resolve(__filename, "..", "..");
|
|
80
|
+
const docsDir = join(ROOT_DIR, settings.docsDir);
|
|
521
81
|
const localeDirs = Object.values(settings.locales ?? {}).map((l) =>
|
|
522
|
-
join(
|
|
82
|
+
join(ROOT_DIR, l.dir),
|
|
523
83
|
);
|
|
524
84
|
const contentDirs = [docsDir, ...localeDirs];
|
|
525
|
-
|
|
526
85
|
const vocabularyActive =
|
|
527
86
|
Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
|
|
528
87
|
|
|
529
88
|
if (flags.fix) {
|
|
530
|
-
const rewrites =
|
|
531
|
-
const touched = await
|
|
89
|
+
const rewrites = coreComputeRewrites(tagVocabulary);
|
|
90
|
+
const touched = await coreApplyFixes(contentDirs, rewrites, ROOT_DIR);
|
|
532
91
|
if (flags.json) {
|
|
533
92
|
process.stdout.write(JSON.stringify({ fixed: touched }, null, 2) + "\n");
|
|
534
93
|
} else if (touched.length === 0) {
|
|
@@ -537,29 +96,32 @@ async function main() {
|
|
|
537
96
|
console.log(pc.green(`✓ Rewrote aliases in ${touched.length} file(s):`));
|
|
538
97
|
for (const f of touched) console.log(` ${f}`);
|
|
539
98
|
}
|
|
540
|
-
|
|
99
|
+
process.exit(0);
|
|
541
100
|
}
|
|
542
101
|
|
|
543
|
-
const report = await
|
|
544
|
-
rootDir,
|
|
102
|
+
const report = await coreAudit({
|
|
103
|
+
rootDir: ROOT_DIR,
|
|
545
104
|
contentDirs,
|
|
546
105
|
vocabulary: tagVocabulary,
|
|
547
106
|
governance: settings.tagGovernance,
|
|
548
107
|
vocabularyActive,
|
|
108
|
+
nearDupHelpers: {
|
|
109
|
+
singular: pluralize.singular,
|
|
110
|
+
compareTwoStrings: stringSimilarity.compareTwoStrings,
|
|
111
|
+
},
|
|
549
112
|
});
|
|
550
113
|
|
|
551
114
|
if (flags.json) {
|
|
552
115
|
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
553
116
|
} else {
|
|
554
|
-
console.log(
|
|
117
|
+
console.log(coreFormatReport(report, settings.tagGovernance, vocabularyActive, pc));
|
|
555
118
|
}
|
|
556
119
|
|
|
557
|
-
const hardIssues =
|
|
120
|
+
const hardIssues = coreHasHardIssues(report);
|
|
558
121
|
if (hardIssues && (flags.ci || settings.tagGovernance === "strict")) {
|
|
559
122
|
process.exit(1);
|
|
560
123
|
}
|
|
561
124
|
if (hardIssues) {
|
|
562
|
-
// Soft mode: surface to stderr so CI users see it in logs while exit 0.
|
|
563
125
|
console.error(
|
|
564
126
|
pc.yellow(
|
|
565
127
|
"Note: tag issues found but running in non-strict mode (exit 0). Use --ci to fail.",
|
|
@@ -567,13 +129,3 @@ async function main() {
|
|
|
567
129
|
);
|
|
568
130
|
}
|
|
569
131
|
}
|
|
570
|
-
|
|
571
|
-
const isMain =
|
|
572
|
-
process.argv[1] !== undefined && resolve(process.argv[1]) === resolve(__filename);
|
|
573
|
-
|
|
574
|
-
if (isMain) {
|
|
575
|
-
main().catch((err) => {
|
|
576
|
-
console.error(err);
|
|
577
|
-
process.exit(1);
|
|
578
|
-
});
|
|
579
|
-
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"identifier": "default",
|
|
3
|
-
"description": "Default capabilities for the main window",
|
|
3
|
+
"description": "Default capabilities for the main window. The shipped offline reader loads the embedded dist/ via the tauri:// protocol (WebviewUrl::App), so it needs no remote-origin grant. The previous `remote.urls: http://localhost:*/**` grant was dropped (zudolab/zudo-doc#2240) so no localhost-served page can reach core:default. `cargo tauri dev` still loads http://localhost:4321, but the zudo-doc site invokes no Tauri commands, so it does not need the grant either.",
|
|
4
4
|
"windows": ["main"],
|
|
5
|
-
"remote": {
|
|
6
|
-
"urls": ["http://localhost:*/**"]
|
|
7
|
-
},
|
|
8
5
|
"permissions": [
|
|
9
6
|
"core:default"
|
|
10
7
|
]
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"app": {
|
|
11
11
|
"windows": [],
|
|
12
12
|
"security": {
|
|
13
|
-
"csp":
|
|
13
|
+
"csp": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data:; font-src 'self' data: https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' https://esm.sh; connect-src 'self' https://esm.sh https://cdn.jsdelivr.net"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"bundle": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"identifier": "default",
|
|
3
|
-
"description": "Default capabilities for the main window",
|
|
3
|
+
"description": "Default capabilities for the main window. TRUST NOTE (zudolab/zudo-doc#2240): Mode 2 displays an arbitrary, user-configured localhost project in this webview, and `remote.urls` grants core:default to any http://localhost:*/** origin. Combined with withGlobalTauri (required by frontend/index.html's launch-error listener + retry_launch invoke), a malicious dependency in the wrapped project would gain Tauri reach. This is an accepted trust assumption for a developer-only wrapper: only point Mode 2 at projects you trust. The grant is intentionally dev-scoped (localhost only). Narrowing it to the single configured port, or splitting the local-frontend capability from the remote-project capability, is tracked as a future hardening.",
|
|
4
4
|
"windows": ["main"],
|
|
5
5
|
"remote": { "urls": ["http://localhost:*/**"] },
|
|
6
6
|
"permissions": ["core:default"]
|