@kenjura/ursa 0.96.0 → 0.97.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 +26 -0
- package/README.md +72 -16
- package/bin/ursa.js +14 -1
- package/meta/templates/default-template/menu.js +18 -1
- package/meta/templates/default-template/search.js +11 -0
- package/meta/templates/default-template/widgets.js +4 -0
- package/package.json +1 -2
- package/src/dev.js +13 -23
- package/src/helper/__test__/contentHash.test.js +16 -6
- package/src/helper/assetBundler.js +93 -19
- package/src/helper/automenu.js +36 -11
- package/src/helper/build/__test__/autoIndex.test.js +2 -132
- package/src/helper/build/__test__/graph.test.js +259 -3
- package/src/helper/build/__test__/pass.test.js +553 -0
- package/src/helper/build/autoIndex.js +2 -371
- package/src/helper/build/excludeFilter.js +1 -2
- package/src/helper/build/footer.js +27 -14
- package/src/helper/build/graph.js +575 -152
- package/src/helper/build/index.js +0 -2
- package/src/helper/build/metadata.js +19 -5
- package/src/helper/build/pass.js +497 -0
- package/src/helper/build/precedence.js +174 -0
- package/src/helper/build/site.js +1270 -0
- package/src/helper/build/templates.js +1 -2
- package/src/helper/build/tracedFs.js +247 -0
- package/src/helper/contentHash.js +0 -78
- package/src/helper/customMenu.js +1 -1
- package/src/helper/fileRenderer.js +119 -111
- package/src/helper/findScriptJs.js +1 -1
- package/src/helper/findStyleCss.js +1 -1
- package/src/helper/folderConfig.js +7 -18
- package/src/helper/fullTextIndex.js +41 -29
- package/src/helper/imageProcessor.js +45 -0
- package/src/helper/linkValidator.js +118 -127
- package/src/helper/mdxRenderer.js +27 -5
- package/src/helper/menuLabels.js +30 -5
- package/src/helper/whitelistFilter.js +1 -2
- package/src/jobs/generate.js +67 -1829
- package/src/serve.js +317 -697
- package/src/helper/__test__/dependencyTracker.test.js +0 -157
- package/src/helper/build/cacheBust.js +0 -141
- package/src/helper/build/navCache.js +0 -145
- package/src/helper/build/watchCache.js +0 -33
- package/src/helper/dependencyTracker.js +0 -384
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { join } from "path";
|
|
2
|
-
import { mkdtemp, rm, readFile } from "fs/promises";
|
|
3
|
-
import { existsSync } from "fs";
|
|
4
|
-
import { tmpdir } from "os";
|
|
5
|
-
import {
|
|
6
|
-
DependencyTracker,
|
|
7
|
-
loadDependencyTracker,
|
|
8
|
-
saveDependencyTracker,
|
|
9
|
-
getDependencyGraphPath,
|
|
10
|
-
} from "../dependencyTracker.js";
|
|
11
|
-
|
|
12
|
-
let tempDir;
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
tempDir = await mkdtemp(join(tmpdir(), "ursa-deptracker-"));
|
|
15
|
-
});
|
|
16
|
-
afterEach(async () => {
|
|
17
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function makeTracker(sourceDir) {
|
|
21
|
-
const tracker = new DependencyTracker();
|
|
22
|
-
tracker.init(sourceDir);
|
|
23
|
-
return tracker;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("serialize / load", () => {
|
|
27
|
-
it("round-trips registrations through serialize + load", () => {
|
|
28
|
-
const t1 = makeTracker("/site/docs");
|
|
29
|
-
t1.registerDocument("/site/docs/a.md", {
|
|
30
|
-
templateName: "default-template",
|
|
31
|
-
cssPaths: ["/site/docs/style.css"],
|
|
32
|
-
scriptPaths: ["/site/docs/script.js"],
|
|
33
|
-
});
|
|
34
|
-
t1.registerDocument("/site/docs/sub/b.md", {
|
|
35
|
-
templateName: "wiki",
|
|
36
|
-
cssPaths: ["/site/docs/style.css", "/site/docs/sub/style.css"],
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const data = JSON.parse(JSON.stringify(t1.serialize()));
|
|
40
|
-
const t2 = makeTracker("/site/docs");
|
|
41
|
-
expect(t2.load(data)).toBe(true);
|
|
42
|
-
|
|
43
|
-
expect([...t2.getAffectedDocuments("/site/docs/style.css")].sort()).toEqual([
|
|
44
|
-
"/site/docs/a.md",
|
|
45
|
-
"/site/docs/sub/b.md",
|
|
46
|
-
]);
|
|
47
|
-
expect([...t2.getDocumentsUsingTemplate("wiki")]).toEqual(["/site/docs/sub/b.md"]);
|
|
48
|
-
expect(t2.getStats()).toEqual(t1.getStats());
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("merges with current-run registrations, which take precedence", () => {
|
|
52
|
-
const t1 = makeTracker("/site/docs");
|
|
53
|
-
t1.registerDocument("/site/docs/a.md", { templateName: "old-template" });
|
|
54
|
-
t1.registerDocument("/site/docs/b.md", { templateName: "default-template" });
|
|
55
|
-
const persisted = t1.serialize();
|
|
56
|
-
|
|
57
|
-
// New run: a.md was re-rendered with a different template before load
|
|
58
|
-
const t2 = makeTracker("/site/docs");
|
|
59
|
-
t2.registerDocument("/site/docs/a.md", { templateName: "new-template" });
|
|
60
|
-
expect(t2.load(persisted)).toBe(true);
|
|
61
|
-
|
|
62
|
-
// Live registration wins; persisted fills in the hash-skipped doc
|
|
63
|
-
expect([...t2.getDocumentsUsingTemplate("new-template")]).toEqual(["/site/docs/a.md"]);
|
|
64
|
-
expect([...t2.getDocumentsUsingTemplate("old-template")]).toEqual([]);
|
|
65
|
-
expect([...t2.getDocumentsUsingTemplate("default-template")]).toEqual(["/site/docs/b.md"]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("rejects mismatched schema versions and source dirs", () => {
|
|
69
|
-
const t = makeTracker("/site/docs");
|
|
70
|
-
expect(t.load(null)).toBe(false);
|
|
71
|
-
expect(t.load({ version: 99, documents: {} })).toBe(false);
|
|
72
|
-
const other = makeTracker("/different/source").serialize();
|
|
73
|
-
other.documents["/different/source/a.md"] = ["template:default-template"];
|
|
74
|
-
expect(t.load(other)).toBe(false);
|
|
75
|
-
expect(t.getStats().totalDocuments).toBe(0);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("prune", () => {
|
|
80
|
-
it("drops registrations for documents not in the keep set", () => {
|
|
81
|
-
const t = makeTracker("/site/docs");
|
|
82
|
-
t.registerDocument("/site/docs/keep.md", { cssPaths: ["/site/docs/style.css"] });
|
|
83
|
-
t.registerDocument("/site/docs/deleted.md", { cssPaths: ["/site/docs/style.css"] });
|
|
84
|
-
|
|
85
|
-
t.prune(new Set(["/site/docs/keep.md"]));
|
|
86
|
-
|
|
87
|
-
expect([...t.getAffectedDocuments("/site/docs/style.css")]).toEqual(["/site/docs/keep.md"]);
|
|
88
|
-
expect(t.getStats().totalDocuments).toBe(1);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("file persistence helpers", () => {
|
|
93
|
-
it("saves to and loads from .ursa/dependency-graph.json", async () => {
|
|
94
|
-
const t1 = makeTracker(tempDir);
|
|
95
|
-
t1.registerDocument(join(tempDir, "a.md"), {
|
|
96
|
-
templateName: "default-template",
|
|
97
|
-
cssPaths: [join(tempDir, "style.css")],
|
|
98
|
-
});
|
|
99
|
-
expect(await saveDependencyTracker(tempDir, t1)).toBe(true);
|
|
100
|
-
expect(existsSync(getDependencyGraphPath(tempDir))).toBe(true);
|
|
101
|
-
const onDisk = JSON.parse(await readFile(getDependencyGraphPath(tempDir), "utf8"));
|
|
102
|
-
expect(onDisk.version).toBe(1);
|
|
103
|
-
|
|
104
|
-
const t2 = makeTracker(tempDir);
|
|
105
|
-
expect(await loadDependencyTracker(tempDir, t2)).toBe(true);
|
|
106
|
-
expect([...t2.getAffectedDocuments(join(tempDir, "style.css"))]).toEqual([
|
|
107
|
-
join(tempDir, "a.md"),
|
|
108
|
-
]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns false when no persisted graph exists", async () => {
|
|
112
|
-
const t = makeTracker(tempDir);
|
|
113
|
-
expect(await loadDependencyTracker(tempDir, t)).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe("getMetaInvalidationPlan", () => {
|
|
118
|
-
it("does not force a full rebuild for static assets in meta", () => {
|
|
119
|
-
const t = makeTracker("/site/docs");
|
|
120
|
-
t.registerDocument("/site/docs/a.md", { templateName: "default-template" });
|
|
121
|
-
|
|
122
|
-
for (const file of ["logo.png", "font.woff2", "manual.pdf", "icon.SVG"]) {
|
|
123
|
-
const plan = t.getMetaInvalidationPlan(`/site/meta/shared/${file}`, "/site/meta");
|
|
124
|
-
expect(plan.requiresFullRebuild).toBe(false);
|
|
125
|
-
expect(plan.affectedDocuments).toEqual([]);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("still regenerates documents for template and css/js meta changes", () => {
|
|
130
|
-
const t = makeTracker("/site/docs");
|
|
131
|
-
t.registerDocument("/site/docs/a.md", { templateName: "default-template" });
|
|
132
|
-
|
|
133
|
-
// New folder structure: templates/{name}/index.html → name from the folder
|
|
134
|
-
const tplPlan = t.getMetaInvalidationPlan(
|
|
135
|
-
"/site/meta/templates/default-template/index.html",
|
|
136
|
-
"/site/meta"
|
|
137
|
-
);
|
|
138
|
-
expect(tplPlan.requiresFullRebuild).toBe(false);
|
|
139
|
-
expect(tplPlan.affectedDocuments).toEqual(["/site/docs/a.md"]);
|
|
140
|
-
|
|
141
|
-
// Legacy flat structure: {name}.html at the meta root
|
|
142
|
-
const legacyPlan = t.getMetaInvalidationPlan(
|
|
143
|
-
"/site/meta/default-template.html",
|
|
144
|
-
"/site/meta"
|
|
145
|
-
);
|
|
146
|
-
expect(legacyPlan.requiresFullRebuild).toBe(false);
|
|
147
|
-
expect(legacyPlan.affectedDocuments).toEqual(["/site/docs/a.md"]);
|
|
148
|
-
|
|
149
|
-
const cssPlan = t.getMetaInvalidationPlan("/site/meta/shared/theme.css", "/site/meta");
|
|
150
|
-
expect(cssPlan.requiresFullRebuild).toBe(false);
|
|
151
|
-
expect(cssPlan.affectedDocuments).toEqual(["/site/docs/a.md"]);
|
|
152
|
-
|
|
153
|
-
// Unknown meta file types still fall back to a full rebuild
|
|
154
|
-
const unknownPlan = t.getMetaInvalidationPlan("/site/meta/shared/data.json", "/site/meta");
|
|
155
|
-
expect(unknownPlan.requiresFullRebuild).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
// Cache busting helpers for build
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generate a cache-busting timestamp in ISO format (e.g., 20251221T221700Z)
|
|
5
|
-
* @returns {string} Timestamp string suitable for query params
|
|
6
|
-
*/
|
|
7
|
-
export function generateCacheBustTimestamp() {
|
|
8
|
-
const now = new Date();
|
|
9
|
-
const year = now.getUTCFullYear();
|
|
10
|
-
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
11
|
-
const day = String(now.getUTCDate()).padStart(2, '0');
|
|
12
|
-
const hours = String(now.getUTCHours()).padStart(2, '0');
|
|
13
|
-
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
|
|
14
|
-
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
|
|
15
|
-
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Add cache-busting timestamp to url() references in CSS content
|
|
20
|
-
* @param {string} cssContent - The CSS file content
|
|
21
|
-
* @param {string} timestamp - The cache-busting timestamp
|
|
22
|
-
* @returns {string} CSS with timestamped URLs
|
|
23
|
-
*/
|
|
24
|
-
export function addTimestampToCssUrls(cssContent, timestamp) {
|
|
25
|
-
// Match url(...) in any context, including CSS variables, with optional whitespace and quotes
|
|
26
|
-
// Exclude data: URLs and already-timestamped URLs
|
|
27
|
-
return cssContent.replace(
|
|
28
|
-
/url\(\s*(['"]?)(?!data:)([^'"\)]+?)\1\s*\)/gi,
|
|
29
|
-
(match, quote, url) => {
|
|
30
|
-
// Don't add timestamp if already has query string
|
|
31
|
-
if (url.includes('?')) {
|
|
32
|
-
return match;
|
|
33
|
-
}
|
|
34
|
-
return `url(${quote}${url}?v=${timestamp}${quote})`;
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Add cache-busting timestamp to static file references in HTML
|
|
41
|
-
* @param {string} html - The HTML content
|
|
42
|
-
* @param {string} timestamp - The cache-busting timestamp
|
|
43
|
-
* @returns {string} HTML with timestamped static file references
|
|
44
|
-
*/
|
|
45
|
-
export function addTimestampToHtmlStaticRefs(html, timestamp) {
|
|
46
|
-
// Add timestamp to CSS links
|
|
47
|
-
html = html.replace(
|
|
48
|
-
/(<link[^>]+href=["'])([^"']+\.css)(["'][^>]*>)/gi,
|
|
49
|
-
`$1$2?v=${timestamp}$3`
|
|
50
|
-
);
|
|
51
|
-
// Add timestamp to JS scripts
|
|
52
|
-
html = html.replace(
|
|
53
|
-
/(<script[^>]+src=["'])([^"']+\.js)(["'][^>]*>)/gi,
|
|
54
|
-
`$1$2?v=${timestamp}$3`
|
|
55
|
-
);
|
|
56
|
-
// Add timestamp to images in img tags
|
|
57
|
-
html = html.replace(
|
|
58
|
-
/(<img[^>]+src=["'])([^"']+\.(jpg|jpeg|png|gif|webp|svg|ico))(["'][^>]*>)/gi,
|
|
59
|
-
`$1$2?v=${timestamp}$4`
|
|
60
|
-
);
|
|
61
|
-
return html;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Generate a short content-based hash for a file's contents.
|
|
66
|
-
* Used for per-file cache-busting so that only changed files invalidate caches.
|
|
67
|
-
* @param {string} content - File content to hash
|
|
68
|
-
* @returns {string} Short hex hash (8 chars)
|
|
69
|
-
*/
|
|
70
|
-
export function generateFileHash(content) {
|
|
71
|
-
let hash = 0;
|
|
72
|
-
for (let i = 0; i < content.length; i++) {
|
|
73
|
-
const char = content.charCodeAt(i);
|
|
74
|
-
hash = ((hash << 5) - hash) + char;
|
|
75
|
-
hash = hash & hash; // Convert to 32-bit integer
|
|
76
|
-
}
|
|
77
|
-
return Math.abs(hash).toString(16).padStart(8, "0").substring(0, 8);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* A cache-bust hash map that stores per-file content hashes.
|
|
82
|
-
* When a static file changes, its hash changes, which invalidates
|
|
83
|
-
* any document referencing it via ?v= query parameters.
|
|
84
|
-
*/
|
|
85
|
-
export class CacheBustHashMap {
|
|
86
|
-
constructor() {
|
|
87
|
-
/** @type {Map<string, string>} relative file path → content hash */
|
|
88
|
-
this.hashes = new Map();
|
|
89
|
-
/** @type {string} fallback timestamp for files not individually tracked */
|
|
90
|
-
this.fallbackTimestamp = generateCacheBustTimestamp();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Update the hash for a file.
|
|
95
|
-
* @param {string} relativePath - File path relative to output dir (e.g., "campaigns/abs/style.css")
|
|
96
|
-
* @param {string} content - File content
|
|
97
|
-
* @returns {string} The new hash
|
|
98
|
-
*/
|
|
99
|
-
update(relativePath, content) {
|
|
100
|
-
const hash = generateFileHash(content);
|
|
101
|
-
this.hashes.set(relativePath, hash);
|
|
102
|
-
return hash;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Get the cache-bust version string for a file.
|
|
107
|
-
* Returns the per-file hash if available, otherwise the fallback timestamp.
|
|
108
|
-
* @param {string} relativePath - File path relative to output dir
|
|
109
|
-
* @returns {string} Version string
|
|
110
|
-
*/
|
|
111
|
-
getVersion(relativePath) {
|
|
112
|
-
return this.hashes.get(relativePath) || this.fallbackTimestamp;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if a file's hash has changed.
|
|
117
|
-
* @param {string} relativePath
|
|
118
|
-
* @param {string} content
|
|
119
|
-
* @returns {boolean} true if the hash differs from the stored value
|
|
120
|
-
*/
|
|
121
|
-
hasChanged(relativePath, content) {
|
|
122
|
-
const newHash = generateFileHash(content);
|
|
123
|
-
const oldHash = this.hashes.get(relativePath);
|
|
124
|
-
return newHash !== oldHash;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Refresh the fallback timestamp (e.g., at the start of a new build).
|
|
129
|
-
*/
|
|
130
|
-
refreshTimestamp() {
|
|
131
|
-
this.fallbackTimestamp = generateCacheBustTimestamp();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get the fallback timestamp (for backward compatibility).
|
|
136
|
-
* @returns {string}
|
|
137
|
-
*/
|
|
138
|
-
get timestamp() {
|
|
139
|
-
return this.fallbackTimestamp;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Navigation cache for build performance
|
|
2
|
-
// Caches the menu structure and only rebuilds when the file list changes
|
|
3
|
-
|
|
4
|
-
import { readFile, writeFile, mkdir, stat } from 'fs/promises';
|
|
5
|
-
import { existsSync, readFileSync } from 'fs';
|
|
6
|
-
import { join, dirname, extname, basename } from 'path';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
|
|
9
|
-
const NAV_CACHE_FILE = 'nav-cache.json';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate a hash of the file list to detect changes
|
|
13
|
-
* @param {string[]} files - Array of file paths
|
|
14
|
-
* @returns {string} Hash of the file list
|
|
15
|
-
*/
|
|
16
|
-
export function hashFileList(files) {
|
|
17
|
-
const sorted = [...files].sort();
|
|
18
|
-
return createHash('md5').update(sorted.join('\n')).digest('hex').substring(0, 16);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate a hash of file stats for detecting content changes
|
|
23
|
-
* Uses mtime and size for speed (avoids reading file contents)
|
|
24
|
-
* @param {string[]} files - Array of file paths to check
|
|
25
|
-
* @returns {Promise<string>} Hash of file stats
|
|
26
|
-
*/
|
|
27
|
-
export async function hashFileStats(files) {
|
|
28
|
-
// Only check index files and config files that affect menu generation
|
|
29
|
-
const relevantFiles = files.filter(f => {
|
|
30
|
-
const base = basename(f).toLowerCase();
|
|
31
|
-
return base === 'index.md' ||
|
|
32
|
-
base === 'index.mdx' ||
|
|
33
|
-
base === 'index.txt' ||
|
|
34
|
-
base === 'index.yml' ||
|
|
35
|
-
base === 'config.json' ||
|
|
36
|
-
base === 'menu.md' ||
|
|
37
|
-
base === 'menu.txt' ||
|
|
38
|
-
base === '_menu.md' ||
|
|
39
|
-
base === '_menu.txt' ||
|
|
40
|
-
base.endsWith('-icon.png') ||
|
|
41
|
-
base.endsWith('-icon.svg') ||
|
|
42
|
-
base === 'icon.png' ||
|
|
43
|
-
base === 'icon.svg';
|
|
44
|
-
}).sort();
|
|
45
|
-
|
|
46
|
-
// Stat files in parallel batches for speed
|
|
47
|
-
const BATCH_SIZE = 100;
|
|
48
|
-
const stats = [];
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < relevantFiles.length; i += BATCH_SIZE) {
|
|
51
|
-
const batch = relevantFiles.slice(i, i + BATCH_SIZE);
|
|
52
|
-
const batchResults = await Promise.all(batch.map(async (file) => {
|
|
53
|
-
try {
|
|
54
|
-
const s = await stat(file);
|
|
55
|
-
return `${file}:${s.mtimeMs}:${s.size}`;
|
|
56
|
-
} catch (e) {
|
|
57
|
-
return null; // File might not exist
|
|
58
|
-
}
|
|
59
|
-
}));
|
|
60
|
-
stats.push(...batchResults.filter(Boolean));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return createHash('md5').update(stats.join('\n')).digest('hex').substring(0, 16);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Load the navigation cache from disk
|
|
68
|
-
* @param {string} sourceDir - Source directory root
|
|
69
|
-
* @returns {Promise<object|null>} Cached nav data or null
|
|
70
|
-
*/
|
|
71
|
-
export async function loadNavCache(sourceDir) {
|
|
72
|
-
const cachePath = join(sourceDir, '.ursa', NAV_CACHE_FILE);
|
|
73
|
-
try {
|
|
74
|
-
if (existsSync(cachePath)) {
|
|
75
|
-
const data = await readFile(cachePath, 'utf8');
|
|
76
|
-
return JSON.parse(data);
|
|
77
|
-
}
|
|
78
|
-
} catch (e) {
|
|
79
|
-
// Ignore errors
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Save the navigation cache to disk
|
|
86
|
-
* @param {string} sourceDir - Source directory root
|
|
87
|
-
* @param {object} cache - Cache data to save
|
|
88
|
-
*/
|
|
89
|
-
export async function saveNavCache(sourceDir, cache) {
|
|
90
|
-
const ursaDir = join(sourceDir, '.ursa');
|
|
91
|
-
const cachePath = join(ursaDir, NAV_CACHE_FILE);
|
|
92
|
-
try {
|
|
93
|
-
await mkdir(ursaDir, { recursive: true });
|
|
94
|
-
await writeFile(cachePath, JSON.stringify(cache, null, 2));
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.warn('Could not save nav cache:', e.message);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check if the navigation cache is valid
|
|
102
|
-
* @param {object} cache - The cached data
|
|
103
|
-
* @param {string} fileListHash - Current file list hash
|
|
104
|
-
* @param {string} fileStatsHash - Current file stats hash
|
|
105
|
-
* @returns {boolean} True if cache is valid
|
|
106
|
-
*/
|
|
107
|
-
export function isNavCacheValid(cache, fileListHash, fileStatsHash) {
|
|
108
|
-
if (!cache) return false;
|
|
109
|
-
if (cache.fileListHash !== fileListHash) return false;
|
|
110
|
-
if (cache.fileStatsHash !== fileStatsHash) return false;
|
|
111
|
-
if (!cache.menuData || !cache.menuHtml || !cache.validPaths) return false;
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Create a cache entry
|
|
117
|
-
* @param {string} fileListHash - Hash of file list
|
|
118
|
-
* @param {string} fileStatsHash - Hash of file stats
|
|
119
|
-
* @param {object} menuData - Menu data structure
|
|
120
|
-
* @param {string} menuHtml - Rendered menu HTML
|
|
121
|
-
* @param {Array} validPathsArray - Valid paths as array of [key, value] pairs
|
|
122
|
-
* @param {Map} customMenus - Custom menus map
|
|
123
|
-
* @returns {object} Cache entry
|
|
124
|
-
*/
|
|
125
|
-
export function createNavCacheEntry(fileListHash, fileStatsHash, menuData, menuHtml, validPathsArray, customMenusArray) {
|
|
126
|
-
return {
|
|
127
|
-
version: 1,
|
|
128
|
-
timestamp: Date.now(),
|
|
129
|
-
fileListHash,
|
|
130
|
-
fileStatsHash,
|
|
131
|
-
menuData,
|
|
132
|
-
menuHtml,
|
|
133
|
-
validPaths: validPathsArray,
|
|
134
|
-
customMenus: customMenusArray,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Restore a Map from cached array
|
|
140
|
-
* @param {Array} arr - Array of [key, value] pairs
|
|
141
|
-
* @returns {Map} Restored map
|
|
142
|
-
*/
|
|
143
|
-
export function restoreMap(arr) {
|
|
144
|
-
return new Map(arr);
|
|
145
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Watch mode cache and clear function for build
|
|
2
|
-
|
|
3
|
-
import { dependencyTracker } from "../dependencyTracker.js";
|
|
4
|
-
|
|
5
|
-
export const watchModeCache = {
|
|
6
|
-
templates: null,
|
|
7
|
-
menu: null,
|
|
8
|
-
footer: null,
|
|
9
|
-
validPaths: null,
|
|
10
|
-
source: null,
|
|
11
|
-
meta: null,
|
|
12
|
-
output: null,
|
|
13
|
-
hashCache: null,
|
|
14
|
-
cacheBustTimestamp: null,
|
|
15
|
-
cacheBustHashes: null, // CacheBustHashMap instance for per-file cache-busting
|
|
16
|
-
allArticlePaths: null, // Array of all article paths (for full rebuild tracking)
|
|
17
|
-
lastFullBuild: 0,
|
|
18
|
-
isInitialized: false,
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function clearWatchCache(cssPathCache) {
|
|
22
|
-
watchModeCache.templates = null;
|
|
23
|
-
watchModeCache.menu = null;
|
|
24
|
-
watchModeCache.footer = null;
|
|
25
|
-
watchModeCache.validPaths = null;
|
|
26
|
-
watchModeCache.hashCache = null;
|
|
27
|
-
watchModeCache.cacheBustHashes = null;
|
|
28
|
-
watchModeCache.allArticlePaths = null;
|
|
29
|
-
watchModeCache.isInitialized = false;
|
|
30
|
-
dependencyTracker.init("");
|
|
31
|
-
if (cssPathCache) cssPathCache.clear();
|
|
32
|
-
console.log('Watch cache cleared');
|
|
33
|
-
}
|