@kenjura/ursa 0.75.0 → 0.77.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 +85 -0
- package/meta/default.css +149 -3
- package/meta/templates/default-template/default.css +1268 -0
- package/meta/{default-template.html → templates/default-template/index.html} +49 -2
- package/meta/{menu.js → templates/default-template/menu.js} +1 -1
- package/meta/templates/default-template/sectionify.js +46 -0
- package/meta/templates/default-template/widgets.js +701 -0
- package/package.json +1 -1
- package/src/dev.js +125 -34
- package/src/helper/assetBundler.js +471 -0
- package/src/helper/build/autoIndex.js +26 -23
- package/src/helper/build/cacheBust.js +79 -0
- package/src/helper/build/navCache.js +4 -0
- package/src/helper/build/templates.js +176 -19
- package/src/helper/build/watchCache.js +7 -0
- package/src/helper/customMenu.js +4 -2
- package/src/helper/dependencyTracker.js +269 -0
- package/src/helper/findScriptJs.js +29 -0
- package/src/helper/findStyleCss.js +29 -0
- package/src/helper/portUtils.js +132 -0
- package/src/jobs/generate.js +276 -59
- package/src/serve.js +446 -162
- package/meta/character-sheet.css +0 -50
- package/meta/widgets.js +0 -376
- /package/meta/{goudy_bookletter_1911-webfont.woff → shared/goudy_bookletter_1911-webfont.woff} +0 -0
- /package/meta/{character-sheet/css → templates/character-sheet-template}/character-sheet.css +0 -0
- /package/meta/{character-sheet/js → templates/character-sheet-template}/components.js +0 -0
- /package/meta/{cssui.bundle.min.css → templates/character-sheet-template/cssui.bundle.min.css} +0 -0
- /package/meta/{character-sheet-template.html → templates/character-sheet-template/index.html} +0 -0
- /package/meta/{character-sheet/js → templates/character-sheet-template}/main.js +0 -0
- /package/meta/{character-sheet/js → templates/character-sheet-template}/model.js +0 -0
- /package/meta/{search.js → templates/default-template/search.js} +0 -0
- /package/meta/{sticky.js → templates/default-template/sticky.js} +0 -0
- /package/meta/{toc-generator.js → templates/default-template/toc-generator.js} +0 -0
- /package/meta/{toc.js → templates/default-template/toc.js} +0 -0
- /package/meta/{template2.html → templates/template2/index.html} +0 -0
|
@@ -1,30 +1,187 @@
|
|
|
1
1
|
// Template helpers for build
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { readFile, readdir } from "fs/promises";
|
|
3
|
+
import { join, basename } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Get all templates from meta directory
|
|
8
|
+
* Templates are now organized in meta/templates/{templateName}/index.html
|
|
9
|
+
* Falls back to legacy flat structure if templates folder doesn't exist
|
|
8
10
|
* @param {string} meta - Full path to meta files directory
|
|
9
|
-
* @returns {Promise<Object>} Map of templateName to
|
|
11
|
+
* @returns {Promise<Object>} Map of templateName to { html, dir }
|
|
10
12
|
*/
|
|
11
13
|
export async function getTemplates(meta) {
|
|
12
|
-
const
|
|
13
|
-
const allHtmlFilenames = allMetaFilenames.filter((filename) =>
|
|
14
|
-
filename.match(/\.html/)
|
|
15
|
-
);
|
|
16
|
-
|
|
14
|
+
const templatesDir = join(meta, "templates");
|
|
17
15
|
let templates = {};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
|
|
17
|
+
// Check if new templates folder structure exists
|
|
18
|
+
if (existsSync(templatesDir)) {
|
|
19
|
+
const entries = await readdir(templatesDir, { withFileTypes: true });
|
|
20
|
+
const templateFolders = entries.filter(e => e.isDirectory());
|
|
21
|
+
|
|
22
|
+
const templatesArray = await Promise.all(
|
|
23
|
+
templateFolders.map(async (folder) => {
|
|
24
|
+
const templateName = folder.name;
|
|
25
|
+
const templateDir = join(templatesDir, templateName);
|
|
26
|
+
const indexPath = join(templateDir, "index.html");
|
|
27
|
+
|
|
28
|
+
if (existsSync(indexPath)) {
|
|
29
|
+
const fileContent = await readFile(indexPath, "utf8");
|
|
30
|
+
return [templateName, { html: fileContent, dir: templateDir }];
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
templatesArray
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.forEach(([name, data]) => (templates[name] = data));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Legacy fallback: check for *-template.html files in meta root
|
|
42
|
+
// This allows backward compatibility during migration
|
|
43
|
+
if (Object.keys(templates).length === 0) {
|
|
44
|
+
const { recurse } = await import("../recursive-readdir.js");
|
|
45
|
+
const allMetaFilenames = await recurse(meta);
|
|
46
|
+
const legacyTemplates = allMetaFilenames.filter((filename) =>
|
|
47
|
+
filename.match(/-template\.html$/)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const templatesArray = await Promise.all(
|
|
51
|
+
legacyTemplates.map(async (filename) => {
|
|
52
|
+
const name = basename(filename).replace("-template.html", "");
|
|
53
|
+
const fileContent = await readFile(filename, "utf8");
|
|
54
|
+
// For legacy templates, the dir is the meta folder itself
|
|
55
|
+
return [name, { html: fileContent, dir: meta }];
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
templatesArray.forEach(
|
|
59
|
+
([templateName, data]) => (templates[templateName] = data)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
28
62
|
|
|
29
63
|
return templates;
|
|
30
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get template HTML content (backward compatible)
|
|
68
|
+
* @param {Object} templates - Templates map from getTemplates()
|
|
69
|
+
* @param {string} name - Template name
|
|
70
|
+
* @returns {string|null} Template HTML or null
|
|
71
|
+
*/
|
|
72
|
+
export function getTemplateHtml(templates, name) {
|
|
73
|
+
const template = templates[name];
|
|
74
|
+
if (!template) return null;
|
|
75
|
+
// Support both new { html, dir } format and legacy string format
|
|
76
|
+
return typeof template === 'string' ? template : template.html;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get template directory (for resolving assets)
|
|
81
|
+
* @param {Object} templates - Templates map from getTemplates()
|
|
82
|
+
* @param {string} name - Template name
|
|
83
|
+
* @returns {string|null} Template directory path or null
|
|
84
|
+
*/
|
|
85
|
+
export function getTemplateDir(templates, name) {
|
|
86
|
+
const template = templates[name];
|
|
87
|
+
if (!template) return null;
|
|
88
|
+
return typeof template === 'string' ? null : template.dir;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Copy meta assets to output/public directory.
|
|
93
|
+
* Handles the new template folder structure:
|
|
94
|
+
* - meta/shared/* → output/public/*
|
|
95
|
+
* - meta/templates/{name}/* (except index.html) → output/public/*
|
|
96
|
+
*
|
|
97
|
+
* Also handles legacy flat structure for backward compatibility.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} metaDir - Path to meta directory
|
|
100
|
+
* @param {string} outputPublicDir - Path to output/public directory
|
|
101
|
+
* @returns {Promise<{ copiedFiles: string[], orphanedFiles: string[] }>}
|
|
102
|
+
*/
|
|
103
|
+
export async function copyMetaAssets(metaDir, outputPublicDir) {
|
|
104
|
+
const { copy } = await import('fs-extra');
|
|
105
|
+
const { readdir, stat } = await import('fs/promises');
|
|
106
|
+
const { relative } = await import('path');
|
|
107
|
+
|
|
108
|
+
const copiedFiles = [];
|
|
109
|
+
const orphanedFiles = [];
|
|
110
|
+
const templatesDir = join(metaDir, 'templates');
|
|
111
|
+
const sharedDir = join(metaDir, 'shared');
|
|
112
|
+
|
|
113
|
+
// Helper to copy a file
|
|
114
|
+
const copyFile = async (src, dest) => {
|
|
115
|
+
await copy(src, dest, { overwrite: true });
|
|
116
|
+
copiedFiles.push(relative(metaDir, src));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Helper to recursively copy directory contents (not the directory itself)
|
|
120
|
+
const copyDirContents = async (srcDir, destDir, excludeFiles = []) => {
|
|
121
|
+
if (!existsSync(srcDir)) return;
|
|
122
|
+
|
|
123
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (entry.name.startsWith('.')) continue; // Skip hidden files
|
|
126
|
+
if (excludeFiles.includes(entry.name)) continue;
|
|
127
|
+
|
|
128
|
+
const srcPath = join(srcDir, entry.name);
|
|
129
|
+
const destPath = join(destDir, entry.name);
|
|
130
|
+
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
await copyDirContents(srcPath, destPath, excludeFiles);
|
|
133
|
+
} else {
|
|
134
|
+
await copyFile(srcPath, destPath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Check if new structure exists
|
|
140
|
+
if (existsSync(templatesDir)) {
|
|
141
|
+
// Copy shared assets
|
|
142
|
+
await copyDirContents(sharedDir, outputPublicDir);
|
|
143
|
+
|
|
144
|
+
// Copy each template's assets (except index.html)
|
|
145
|
+
const templateFolders = await readdir(templatesDir, { withFileTypes: true });
|
|
146
|
+
for (const folder of templateFolders) {
|
|
147
|
+
if (!folder.isDirectory()) continue;
|
|
148
|
+
const templateDir = join(templatesDir, folder.name);
|
|
149
|
+
await copyDirContents(templateDir, outputPublicDir, ['index.html']);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for orphaned files in meta root (excluding templates and shared folders)
|
|
153
|
+
const rootEntries = await readdir(metaDir, { withFileTypes: true });
|
|
154
|
+
for (const entry of rootEntries) {
|
|
155
|
+
if (entry.name.startsWith('.')) continue;
|
|
156
|
+
if (entry.name === 'templates' || entry.name === 'shared') continue;
|
|
157
|
+
|
|
158
|
+
// This is an orphaned file/folder in meta root
|
|
159
|
+
const fullPath = join(metaDir, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
// Recursively find all orphaned files
|
|
162
|
+
const findOrphans = async (dir) => {
|
|
163
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
if (e.name.startsWith('.')) continue;
|
|
166
|
+
const p = join(dir, e.name);
|
|
167
|
+
if (e.isDirectory()) {
|
|
168
|
+
await findOrphans(p);
|
|
169
|
+
} else {
|
|
170
|
+
orphanedFiles.push(relative(metaDir, p));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await findOrphans(fullPath);
|
|
175
|
+
} else {
|
|
176
|
+
orphanedFiles.push(entry.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Legacy: copy entire meta folder to public (old behavior)
|
|
181
|
+
await copy(metaDir, outputPublicDir, {
|
|
182
|
+
filter: (src) => !basename(src).startsWith('.') && !src.endsWith('.html')
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { copiedFiles, orphanedFiles };
|
|
187
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Watch mode cache and clear function for build
|
|
2
2
|
|
|
3
|
+
import { dependencyTracker } from "../dependencyTracker.js";
|
|
4
|
+
|
|
3
5
|
export const watchModeCache = {
|
|
4
6
|
templates: null,
|
|
5
7
|
menu: null,
|
|
@@ -10,6 +12,8 @@ export const watchModeCache = {
|
|
|
10
12
|
output: null,
|
|
11
13
|
hashCache: null,
|
|
12
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)
|
|
13
17
|
lastFullBuild: 0,
|
|
14
18
|
isInitialized: false,
|
|
15
19
|
};
|
|
@@ -20,7 +24,10 @@ export function clearWatchCache(cssPathCache) {
|
|
|
20
24
|
watchModeCache.footer = null;
|
|
21
25
|
watchModeCache.validPaths = null;
|
|
22
26
|
watchModeCache.hashCache = null;
|
|
27
|
+
watchModeCache.cacheBustHashes = null;
|
|
28
|
+
watchModeCache.allArticlePaths = null;
|
|
23
29
|
watchModeCache.isInitialized = false;
|
|
30
|
+
dependencyTracker.init("");
|
|
24
31
|
if (cssPathCache) cssPathCache.clear();
|
|
25
32
|
console.log('Watch cache cleared');
|
|
26
33
|
}
|
package/src/helper/customMenu.js
CHANGED
|
@@ -267,11 +267,13 @@ export function autoGenerateMenuFromFolder(folderPath, sourceRoot, depth = 10, i
|
|
|
267
267
|
const topLevelFolders = processedItems.filter(item => item.hasChildren);
|
|
268
268
|
const topLevelFiles = processedItems.filter(item => !item.hasChildren);
|
|
269
269
|
|
|
270
|
-
const
|
|
270
|
+
const relPath = relative(sourceRoot, folderPath).replace(/\\/g, '/');
|
|
271
|
+
// Avoid double slash: if relPath is empty (root), href is '/index.html', otherwise '/relPath/index.html'
|
|
272
|
+
const homeHref = relPath ? `/${relPath}/index.html` : '/index.html';
|
|
271
273
|
const homeItem = {
|
|
272
274
|
label: 'Home',
|
|
273
275
|
path: 'home',
|
|
274
|
-
href:
|
|
276
|
+
href: homeHref,
|
|
275
277
|
hasChildren: topLevelFiles.length > 0,
|
|
276
278
|
icon: `<span class="menu-icon">${HOME_ICON}</span>`,
|
|
277
279
|
children: topLevelFiles,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency tracker for Ursa's regeneration system.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which documents depend on which files so that when a file changes,
|
|
5
|
+
* we can determine exactly which documents need regeneration.
|
|
6
|
+
*
|
|
7
|
+
* Dependency types:
|
|
8
|
+
* - template: document uses a specific meta template
|
|
9
|
+
* - style: document inherits a specific style.css
|
|
10
|
+
* - script: document inherits a specific script.js
|
|
11
|
+
* - static: document references a static asset (image, font, etc.)
|
|
12
|
+
* - meta-asset: document depends on a meta CSS/JS file (via template bundling)
|
|
13
|
+
*
|
|
14
|
+
* The tracker maintains two indexes:
|
|
15
|
+
* 1. fileToDocuments: Map<dependencyPath, Set<documentPath>> — given a changed file, which documents need regeneration?
|
|
16
|
+
* 2. documentToFiles: Map<documentPath, Set<dependencyPath>> — given a document, what are its dependencies? (for cleanup)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { dirname, join, relative, resolve } from "path";
|
|
20
|
+
|
|
21
|
+
export class DependencyTracker {
|
|
22
|
+
constructor() {
|
|
23
|
+
/** @type {Map<string, Set<string>>} dependency file path → set of document paths */
|
|
24
|
+
this.fileToDocuments = new Map();
|
|
25
|
+
/** @type {Map<string, Set<string>>} document path → set of dependency file paths */
|
|
26
|
+
this.documentToFiles = new Map();
|
|
27
|
+
/** @type {string} source directory root (absolute, with trailing slash) */
|
|
28
|
+
this.sourceDir = "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize with the source directory.
|
|
33
|
+
* @param {string} sourceDir - Absolute path to source directory (with or without trailing slash)
|
|
34
|
+
*/
|
|
35
|
+
init(sourceDir) {
|
|
36
|
+
this.sourceDir = resolve(sourceDir) + "/";
|
|
37
|
+
this.fileToDocuments.clear();
|
|
38
|
+
this.documentToFiles.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register that a document depends on a file.
|
|
43
|
+
* @param {string} documentPath - Absolute path to the document
|
|
44
|
+
* @param {string} dependencyPath - Absolute path to the dependency file
|
|
45
|
+
*/
|
|
46
|
+
addDependency(documentPath, dependencyPath) {
|
|
47
|
+
// file → documents
|
|
48
|
+
if (!this.fileToDocuments.has(dependencyPath)) {
|
|
49
|
+
this.fileToDocuments.set(dependencyPath, new Set());
|
|
50
|
+
}
|
|
51
|
+
this.fileToDocuments.get(dependencyPath).add(documentPath);
|
|
52
|
+
|
|
53
|
+
// document → files
|
|
54
|
+
if (!this.documentToFiles.has(documentPath)) {
|
|
55
|
+
this.documentToFiles.set(documentPath, new Set());
|
|
56
|
+
}
|
|
57
|
+
this.documentToFiles.get(documentPath).add(dependencyPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Register dependencies for a document: template, style.css files, script.js files.
|
|
62
|
+
* @param {string} documentPath - Absolute path to the document
|
|
63
|
+
* @param {{ templateName: string, cssPaths: string[], scriptPaths: string[], metaAssets: string[] }} deps
|
|
64
|
+
*/
|
|
65
|
+
registerDocument(documentPath, { templateName, cssPaths = [], scriptPaths = [], metaAssets = [] } = {}) {
|
|
66
|
+
// Clear old dependencies for this document
|
|
67
|
+
this.clearDocument(documentPath);
|
|
68
|
+
|
|
69
|
+
// Template dependency (use a virtual path so template changes can be looked up)
|
|
70
|
+
if (templateName) {
|
|
71
|
+
this.addDependency(documentPath, `template:${templateName}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Style.css dependencies
|
|
75
|
+
for (const cssPath of cssPaths) {
|
|
76
|
+
this.addDependency(documentPath, cssPath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Script.js dependencies
|
|
80
|
+
for (const scriptPath of scriptPaths) {
|
|
81
|
+
this.addDependency(documentPath, scriptPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Meta template assets (CSS/JS referenced by the template)
|
|
85
|
+
for (const metaAsset of metaAssets) {
|
|
86
|
+
this.addDependency(documentPath, metaAsset);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear all dependencies for a document.
|
|
92
|
+
* @param {string} documentPath
|
|
93
|
+
*/
|
|
94
|
+
clearDocument(documentPath) {
|
|
95
|
+
const deps = this.documentToFiles.get(documentPath);
|
|
96
|
+
if (deps) {
|
|
97
|
+
for (const dep of deps) {
|
|
98
|
+
const docSet = this.fileToDocuments.get(dep);
|
|
99
|
+
if (docSet) {
|
|
100
|
+
docSet.delete(documentPath);
|
|
101
|
+
if (docSet.size === 0) this.fileToDocuments.delete(dep);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.documentToFiles.delete(documentPath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all documents that depend on a given file.
|
|
110
|
+
* @param {string} filePath - Absolute path to the changed file
|
|
111
|
+
* @returns {Set<string>} Set of document paths that need regeneration
|
|
112
|
+
*/
|
|
113
|
+
getAffectedDocuments(filePath) {
|
|
114
|
+
return this.fileToDocuments.get(filePath) || new Set();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all documents that use a specific template.
|
|
119
|
+
* @param {string} templateName - Template name (e.g., "default-template")
|
|
120
|
+
* @returns {Set<string>} Set of document paths
|
|
121
|
+
*/
|
|
122
|
+
getDocumentsUsingTemplate(templateName) {
|
|
123
|
+
return this.getAffectedDocuments(`template:${templateName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Determine which documents are affected by a changed file.
|
|
128
|
+
* This is the main entry point for the invalidation logic.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} changedFile - Absolute path to the changed file
|
|
131
|
+
* @param {string} sourceDir - Absolute path to source directory
|
|
132
|
+
* @returns {{ affectedDocuments: string[], reason: string, requiresFullRebuild: boolean }}
|
|
133
|
+
*/
|
|
134
|
+
getInvalidationPlan(changedFile, sourceDir) {
|
|
135
|
+
const normalizedSource = resolve(sourceDir) + "/";
|
|
136
|
+
const relativePath = changedFile.replace(normalizedSource, "");
|
|
137
|
+
const fileName = relativePath.split("/").pop();
|
|
138
|
+
|
|
139
|
+
// 1. Menu/config changes → full rebuild (affects navigation structure)
|
|
140
|
+
if (
|
|
141
|
+
fileName === "menu.md" || fileName === "menu.txt" || fileName === "_menu" ||
|
|
142
|
+
fileName === "config.json" || fileName === "_config"
|
|
143
|
+
) {
|
|
144
|
+
return {
|
|
145
|
+
affectedDocuments: [],
|
|
146
|
+
reason: `Menu/config change: ${relativePath}`,
|
|
147
|
+
requiresFullRebuild: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. style.css or script.js → affects current folder + all subfolders
|
|
152
|
+
// These are "inherited" files — documents in the folder and all subfolders include them.
|
|
153
|
+
if (
|
|
154
|
+
fileName === "style.css" || fileName === "_style.css" || fileName === "style-ursa.css" ||
|
|
155
|
+
fileName === "script.js" || fileName === "_script.js"
|
|
156
|
+
) {
|
|
157
|
+
// Get all documents directly registered as depending on this file
|
|
158
|
+
const directDeps = this.getAffectedDocuments(changedFile);
|
|
159
|
+
|
|
160
|
+
if (directDeps.size > 0) {
|
|
161
|
+
return {
|
|
162
|
+
affectedDocuments: [...directDeps],
|
|
163
|
+
reason: `Inherited ${fileName} changed: ${relativePath} (${directDeps.size} documents)`,
|
|
164
|
+
requiresFullRebuild: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fallback: if dependency tracker wasn't populated, scope by directory
|
|
169
|
+
const changedDir = dirname(changedFile);
|
|
170
|
+
const allDocs = [...this.documentToFiles.keys()];
|
|
171
|
+
const affected = allDocs.filter((doc) => doc.startsWith(changedDir));
|
|
172
|
+
return {
|
|
173
|
+
affectedDocuments: affected,
|
|
174
|
+
reason: `Inherited ${fileName} changed: ${relativePath} (${affected.length} documents in subtree, fallback)`,
|
|
175
|
+
requiresFullRebuild: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 3. Article file changes → just that document
|
|
180
|
+
if (/\.(md|mdx|txt|yml|yaml)$/.test(fileName)) {
|
|
181
|
+
return {
|
|
182
|
+
affectedDocuments: [changedFile],
|
|
183
|
+
reason: `Article changed: ${relativePath}`,
|
|
184
|
+
requiresFullRebuild: false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 4. Other static files (images, fonts, etc.) → find documents that reference them
|
|
189
|
+
const directDeps = this.getAffectedDocuments(changedFile);
|
|
190
|
+
if (directDeps.size > 0) {
|
|
191
|
+
return {
|
|
192
|
+
affectedDocuments: [...directDeps],
|
|
193
|
+
reason: `Static asset changed: ${relativePath} (${directDeps.size} referencing documents)`,
|
|
194
|
+
requiresFullRebuild: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If we don't track this file, it's safe to just broadcast reload (dev mode only)
|
|
199
|
+
return {
|
|
200
|
+
affectedDocuments: [],
|
|
201
|
+
reason: `Unknown file changed: ${relativePath} (no registered dependents)`,
|
|
202
|
+
requiresFullRebuild: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Determine which documents are affected by a meta file change.
|
|
208
|
+
* @param {string} changedFile - Absolute path to the changed meta file
|
|
209
|
+
* @param {string} metaDir - Absolute path to meta directory
|
|
210
|
+
* @returns {{ affectedDocuments: string[], reason: string, requiresFullRebuild: boolean }}
|
|
211
|
+
*/
|
|
212
|
+
getMetaInvalidationPlan(changedFile, metaDir) {
|
|
213
|
+
const normalizedMeta = resolve(metaDir) + "/";
|
|
214
|
+
const relativePath = changedFile.replace(normalizedMeta, "");
|
|
215
|
+
const fileName = relativePath.split("/").pop();
|
|
216
|
+
|
|
217
|
+
// Template file changed → regenerate all documents using that template
|
|
218
|
+
if (fileName.endsWith(".html")) {
|
|
219
|
+
const templateName = fileName.replace(".html", "");
|
|
220
|
+
const affected = this.getDocumentsUsingTemplate(templateName);
|
|
221
|
+
if (affected.size > 0) {
|
|
222
|
+
return {
|
|
223
|
+
affectedDocuments: [...affected],
|
|
224
|
+
reason: `Template changed: ${templateName} (${affected.size} documents)`,
|
|
225
|
+
requiresFullRebuild: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// If no documents tracked, fall back to full rebuild (safe default)
|
|
229
|
+
return {
|
|
230
|
+
affectedDocuments: [],
|
|
231
|
+
reason: `Template changed: ${templateName} (no tracked documents, full rebuild)`,
|
|
232
|
+
requiresFullRebuild: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Meta CSS or JS file changed → all documents are affected
|
|
237
|
+
// (meta assets are bundled into every template)
|
|
238
|
+
if (fileName.endsWith(".css") || fileName.endsWith(".js")) {
|
|
239
|
+
const allDocs = [...this.documentToFiles.keys()];
|
|
240
|
+
return {
|
|
241
|
+
affectedDocuments: allDocs,
|
|
242
|
+
reason: `Meta asset changed: ${relativePath} (affects all ${allDocs.length} documents)`,
|
|
243
|
+
requiresFullRebuild: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Other meta file → full rebuild to be safe
|
|
248
|
+
return {
|
|
249
|
+
affectedDocuments: [],
|
|
250
|
+
reason: `Meta file changed: ${relativePath}`,
|
|
251
|
+
requiresFullRebuild: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get stats about the dependency graph.
|
|
257
|
+
* @returns {{ totalDocuments: number, totalDependencies: number, uniqueFiles: number }}
|
|
258
|
+
*/
|
|
259
|
+
getStats() {
|
|
260
|
+
return {
|
|
261
|
+
totalDocuments: this.documentToFiles.size,
|
|
262
|
+
totalDependencies: [...this.documentToFiles.values()].reduce((sum, s) => sum + s.size, 0),
|
|
263
|
+
uniqueFiles: this.fileToDocuments.size,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Singleton instance
|
|
269
|
+
export const dependencyTracker = new DependencyTracker();
|
|
@@ -24,3 +24,32 @@ export async function findScriptJs(startDir, names = ["script.js", "_script.js"]
|
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find ALL script.js or _script.js files from the docroot down to startDir.
|
|
30
|
+
* Walks up from startDir to docroot collecting all matches, then returns them
|
|
31
|
+
* sorted from shortest path (closest to docroot) to longest (closest to startDir).
|
|
32
|
+
* @param {string} startDir - Directory to start searching from (deepest)
|
|
33
|
+
* @param {string} docroot - The root directory to stop at (shallowest)
|
|
34
|
+
* @param {string[]} [names=["script.js", "_script.js"]] - Filenames to look for
|
|
35
|
+
* @returns {Promise<string[]>} Array of script file paths, ordered from shallowest to deepest
|
|
36
|
+
*/
|
|
37
|
+
export async function findAllScriptJs(startDir, docroot, names = ["script.js", "_script.js"]) {
|
|
38
|
+
const found = [];
|
|
39
|
+
let dir = resolve(startDir);
|
|
40
|
+
const base = resolve(docroot);
|
|
41
|
+
while (true) {
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const candidate = join(dir, name);
|
|
44
|
+
if (existsSync(candidate)) {
|
|
45
|
+
found.push(candidate);
|
|
46
|
+
break; // Only one match per directory (prefer script.js over _script.js)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (dir === base || dir === dirname(dir)) break;
|
|
50
|
+
dir = dirname(dir);
|
|
51
|
+
}
|
|
52
|
+
// Sort from shortest path (docroot) to longest (startDir)
|
|
53
|
+
found.sort((a, b) => a.length - b.length);
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
@@ -24,3 +24,32 @@ export async function findStyleCss(startDir, names = ["style-ursa.css", "style.c
|
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find ALL style.css or _style.css files from the docroot down to startDir.
|
|
30
|
+
* Walks up from startDir to docroot collecting all matches, then returns them
|
|
31
|
+
* sorted from shortest path (closest to docroot) to longest (closest to startDir).
|
|
32
|
+
* @param {string} startDir - Directory to start searching from (deepest)
|
|
33
|
+
* @param {string} docroot - The root directory to stop at (shallowest)
|
|
34
|
+
* @param {string[]} [names=["style-ursa.css", "style.css", "_style.css"]] - Filenames to look for
|
|
35
|
+
* @returns {Promise<string[]>} Array of CSS file paths, ordered from shallowest to deepest
|
|
36
|
+
*/
|
|
37
|
+
export async function findAllStyleCss(startDir, docroot, names = ["style-ursa.css", "style.css", "_style.css"]) {
|
|
38
|
+
const found = [];
|
|
39
|
+
let dir = resolve(startDir);
|
|
40
|
+
const base = resolve(docroot);
|
|
41
|
+
while (true) {
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const candidate = join(dir, name);
|
|
44
|
+
if (existsSync(candidate)) {
|
|
45
|
+
found.push(candidate);
|
|
46
|
+
break; // Only one match per directory (prefer style-ursa.css > style.css > _style.css)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (dir === base || dir === dirname(dir)) break;
|
|
50
|
+
dir = dirname(dir);
|
|
51
|
+
}
|
|
52
|
+
// Sort from shortest path (docroot) to longest (startDir)
|
|
53
|
+
found.sort((a, b) => a.length - b.length);
|
|
54
|
+
return found;
|
|
55
|
+
}
|