@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,13 +1,6 @@
|
|
|
1
1
|
// Auto-index generation helpers for build
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { basename, dirname, extname, join } from "path";
|
|
5
|
-
import { outputFile } from "fs-extra";
|
|
6
|
-
import { findStyleCss, findAllStyleCss } from "../findStyleCss.js";
|
|
7
|
-
import { findAllScriptJs } from "../findScriptJs.js";
|
|
8
|
-
import { addTimestampToHtmlStaticRefs } from "./cacheBust.js";
|
|
9
|
-
import { isMetadataOnly, extractMetadata, getAutoIndexConfig } from "../metadataExtractor.js";
|
|
10
|
-
import { getCustomMenuForFile } from "./menu.js";
|
|
2
|
+
import { readdir } from "./tracedFs.js";
|
|
3
|
+
import { basename, extname, join } from "path";
|
|
11
4
|
import { getFolderConfig, isFolderSelfHidden } from "../folderConfig.js";
|
|
12
5
|
import {
|
|
13
6
|
toDisplayName,
|
|
@@ -17,12 +10,9 @@ import {
|
|
|
17
10
|
getFileLabel,
|
|
18
11
|
findSourceDocument,
|
|
19
12
|
} from "../menuLabels.js";
|
|
20
|
-
import { generateBreadcrumbs } from "../breadcrumbs.js";
|
|
21
|
-
import { bundleDocumentCss, bundleDocumentJs } from "../assetBundler.js";
|
|
22
13
|
|
|
23
14
|
// Document extensions for checking if a folder has content
|
|
24
15
|
const SOURCE_DOC_EXTENSIONS = ['.md', '.mdx', '.txt', '.yml', '.html'];
|
|
25
|
-
const OUTPUT_DOC_EXTENSIONS = ['.html'];
|
|
26
16
|
|
|
27
17
|
/**
|
|
28
18
|
* Recursively check if a directory contains any document files.
|
|
@@ -95,87 +85,6 @@ function compareEntries(a, b) {
|
|
|
95
85
|
return a.sortKey.toLowerCase().localeCompare(b.sortKey.toLowerCase());
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
/**
|
|
99
|
-
* Generate auto-index HTML content for a directory from the OUTPUT folder
|
|
100
|
-
* (used by fallback auto-index generation after all files are generated)
|
|
101
|
-
* @param {string} dir - The directory path to generate index for (in output folder)
|
|
102
|
-
* @param {number} depth - How deep to recurse (1 = current level only, 2 = current + children, etc.)
|
|
103
|
-
* @param {number} [currentDepth=0] - Current recursion depth (internal use)
|
|
104
|
-
* @param {string} [pathPrefix=''] - Path prefix for generating correct hrefs (internal use)
|
|
105
|
-
* @param {string|null} [sourceDir=null] - Matching source directory, so `menu-label`
|
|
106
|
-
* frontmatter and config.json labels can be honored. Without it, entries fall
|
|
107
|
-
* back to their prettified file names.
|
|
108
|
-
* @returns {Promise<string>} HTML content for the auto-index
|
|
109
|
-
*/
|
|
110
|
-
export async function generateAutoIndexHtml(dir, depth = 1, currentDepth = 0, pathPrefix = '', sourceDir = null) {
|
|
111
|
-
try {
|
|
112
|
-
const children = await readdir(dir, { withFileTypes: true });
|
|
113
|
-
|
|
114
|
-
// Filter to only include relevant files and folders
|
|
115
|
-
const filteredChildren = children
|
|
116
|
-
.filter(child => {
|
|
117
|
-
// Skip hidden files
|
|
118
|
-
if (child.name.startsWith('.')) return false;
|
|
119
|
-
// Skip index.html - we're generating it or it's the current page
|
|
120
|
-
if (child.name === 'index.html') return false;
|
|
121
|
-
// Skip img folders (contain images, not content)
|
|
122
|
-
if (child.isDirectory() && child.name === 'img') return false;
|
|
123
|
-
// Skip folders config.json marks hidden — they are ignored entirely,
|
|
124
|
-
// so a stale output directory must not resurrect them in a listing
|
|
125
|
-
if (child.isDirectory() && sourceDir && isFolderSelfHidden(join(sourceDir, child.name))) return false;
|
|
126
|
-
// Include directories and html files
|
|
127
|
-
return child.isDirectory() || child.name.endsWith('.html');
|
|
128
|
-
})
|
|
129
|
-
.map(child => {
|
|
130
|
-
const isDir = child.isDirectory();
|
|
131
|
-
const baseName = isDir ? child.name : child.name.replace(/\.html$/, '');
|
|
132
|
-
return { child, isDir, baseName, ...resolveEntryNaming(isDir, baseName, sourceDir) };
|
|
133
|
-
})
|
|
134
|
-
.sort(compareEntries);
|
|
135
|
-
|
|
136
|
-
if (filteredChildren.length === 0) {
|
|
137
|
-
return '';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const items = [];
|
|
141
|
-
|
|
142
|
-
for (const { child, isDir, label } of filteredChildren) {
|
|
143
|
-
// Skip directories that contain no documents
|
|
144
|
-
if (isDir) {
|
|
145
|
-
const childDir = join(dir, child.name);
|
|
146
|
-
const childSource = sourceDir ? join(sourceDir, child.name) : null;
|
|
147
|
-
if (!await directoryHasDocuments(childDir, OUTPUT_DOC_EXTENSIONS, childSource)) continue;
|
|
148
|
-
}
|
|
149
|
-
// Use pathPrefix to ensure hrefs are correct relative to the document root
|
|
150
|
-
const childPath = pathPrefix ? `${pathPrefix}/${child.name}` : child.name;
|
|
151
|
-
const href = isDir ? `${childPath}/index.html` : (pathPrefix ? `${pathPrefix}/${child.name}` : child.name);
|
|
152
|
-
const icon = isDir ? '📁' : '📄';
|
|
153
|
-
|
|
154
|
-
let itemHtml = `<li>${icon} <a href="${href}">${label}</a>`;
|
|
155
|
-
|
|
156
|
-
// If this is a directory and we need to go deeper, recurse
|
|
157
|
-
if (isDir && currentDepth + 1 < depth) {
|
|
158
|
-
const childDir = join(dir, child.name);
|
|
159
|
-
const childHtml = await generateAutoIndexHtml(
|
|
160
|
-
childDir, depth, currentDepth + 1, childPath,
|
|
161
|
-
sourceDir ? join(sourceDir, child.name) : null
|
|
162
|
-
);
|
|
163
|
-
if (childHtml) {
|
|
164
|
-
itemHtml += `\n${childHtml}`;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
itemHtml += '</li>';
|
|
169
|
-
items.push(itemHtml);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return `<ul class="auto-index depth-${currentDepth + 1}">\n${items.join('\n')}\n</ul>`;
|
|
173
|
-
} catch (e) {
|
|
174
|
-
console.error(`Error generating auto-index HTML for ${dir}: ${e.message}`);
|
|
175
|
-
return '';
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
88
|
/**
|
|
180
89
|
* Generate auto-index HTML content from the SOURCE folder
|
|
181
90
|
* (used for inline auto-index generation in index.md files with generate-auto-index: true)
|
|
@@ -250,281 +159,3 @@ export async function generateAutoIndexHtmlFromSource(sourceDir, depth = 1, curr
|
|
|
250
159
|
return '';
|
|
251
160
|
}
|
|
252
161
|
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Generate automatic index.html files for folders that don't have one
|
|
256
|
-
* @param {string} output - Output directory path
|
|
257
|
-
* @param {string[]} directories - List of source directories
|
|
258
|
-
* @param {string} source - Source directory path
|
|
259
|
-
* @param {object} templates - Template map
|
|
260
|
-
* @param {string} menu - Rendered menu HTML
|
|
261
|
-
* @param {string} footer - Footer HTML
|
|
262
|
-
* @param {string[]} generatedArticles - List of source article paths that were generated
|
|
263
|
-
* @param {Set<string>} copiedCssFiles - Set of CSS files already copied to output
|
|
264
|
-
* @param {Set<string>} existingHtmlFiles - Set of existing HTML files in source (relative paths)
|
|
265
|
-
* @param {string} cacheBustTimestamp - Cache-busting timestamp
|
|
266
|
-
* @param {object} progress - Progress reporter instance
|
|
267
|
-
* @param {Map} customMenus - Map of custom menus by directory path
|
|
268
|
-
*/
|
|
269
|
-
export async function generateAutoIndices(output, directories, source, templates, menu, footer, generatedArticles, copiedCssFiles, existingHtmlFiles, cacheBustTimestamp, progress, customMenus) {
|
|
270
|
-
// Alternate index file names to look for (in priority order)
|
|
271
|
-
const INDEX_ALTERNATES = ['_index.html', 'home.html', '_home.html'];
|
|
272
|
-
|
|
273
|
-
// Normalize paths (remove trailing slashes for consistent replacement)
|
|
274
|
-
const sourceNorm = source.replace(/\/+$/, '');
|
|
275
|
-
const outputNorm = output.replace(/\/+$/, '');
|
|
276
|
-
|
|
277
|
-
// Build set of directories that already have an index.html from a source index.md/txt/yml
|
|
278
|
-
// Exclude metadata-only index files - they should still get auto-generated indices
|
|
279
|
-
const dirsWithSourceIndex = new Set();
|
|
280
|
-
for (const articlePath of generatedArticles) {
|
|
281
|
-
const base = basename(articlePath, extname(articlePath));
|
|
282
|
-
if (base === 'index') {
|
|
283
|
-
// Check if this is a metadata-only index file
|
|
284
|
-
try {
|
|
285
|
-
if (existsSync(articlePath)) {
|
|
286
|
-
const content = readFileSync(articlePath, 'utf8');
|
|
287
|
-
if (isMetadataOnly(content)) {
|
|
288
|
-
// Skip - this folder should still get an auto-index
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
} catch (e) {
|
|
293
|
-
// If we can't read it, assume it has content
|
|
294
|
-
}
|
|
295
|
-
const dir = dirname(articlePath);
|
|
296
|
-
const outputDir = dir.replace(sourceNorm, outputNorm);
|
|
297
|
-
dirsWithSourceIndex.add(outputDir);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Get all output directories (including root)
|
|
302
|
-
const outputDirs = new Set([outputNorm]);
|
|
303
|
-
for (const dir of directories) {
|
|
304
|
-
// Handle both with and without trailing slash in source
|
|
305
|
-
const outputDir = dir.replace(sourceNorm, outputNorm);
|
|
306
|
-
outputDirs.add(outputDir);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
let generatedCount = 0;
|
|
310
|
-
let renamedCount = 0;
|
|
311
|
-
let skippedHtmlCount = 0;
|
|
312
|
-
|
|
313
|
-
for (const dir of outputDirs) {
|
|
314
|
-
// Skip output directories that were never created (the source folder
|
|
315
|
-
// produced no output files, e.g. it was empty)
|
|
316
|
-
if (!existsSync(dir)) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const indexPath = join(dir, 'index.html');
|
|
321
|
-
|
|
322
|
-
// Skip if this directory had a source index.md/txt/yml that was already processed
|
|
323
|
-
if (dirsWithSourceIndex.has(dir)) {
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Check if there's an existing index.html in the source directory (don't overwrite it)
|
|
328
|
-
const sourceDir = dir.replace(outputNorm, sourceNorm);
|
|
329
|
-
const relativeIndexPath = join(sourceDir, 'index.html').replace(sourceNorm + '/', '');
|
|
330
|
-
if (existingHtmlFiles && existingHtmlFiles.has(relativeIndexPath)) {
|
|
331
|
-
skippedHtmlCount++;
|
|
332
|
-
continue; // Don't overwrite existing source HTML
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// NOTE: an existing index.html in output is deliberately NOT a reason to
|
|
336
|
-
// skip. This listing describes the folder's contents, so it goes stale the
|
|
337
|
-
// moment a document or subfolder is added or removed, and warm builds kept
|
|
338
|
-
// whatever was written the first time — indefinitely. Everything that has a
|
|
339
|
-
// rightful claim on index.html is already excluded above: folders with a
|
|
340
|
-
// source index document (dirsWithSourceIndex, built from every source
|
|
341
|
-
// article rather than only the regenerated ones) and hand-written source
|
|
342
|
-
// HTML (existingHtmlFiles). Alternates are re-promoted below, so those stay
|
|
343
|
-
// authoritative too.
|
|
344
|
-
|
|
345
|
-
// Get folder name for (foldername).html check
|
|
346
|
-
const folderName = basename(dir);
|
|
347
|
-
const folderNameAlternate = `${folderName}.html`;
|
|
348
|
-
|
|
349
|
-
// Check for alternate index files
|
|
350
|
-
let foundAlternate = null;
|
|
351
|
-
for (const alt of [...INDEX_ALTERNATES, folderNameAlternate]) {
|
|
352
|
-
const altPath = join(dir, alt);
|
|
353
|
-
if (existsSync(altPath)) {
|
|
354
|
-
foundAlternate = altPath;
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (foundAlternate) {
|
|
360
|
-
// Rename/copy alternate to index.html
|
|
361
|
-
try {
|
|
362
|
-
const content = await readFile(foundAlternate, 'utf8');
|
|
363
|
-
await outputFile(indexPath, content);
|
|
364
|
-
renamedCount++;
|
|
365
|
-
progress.status('Auto-index', `Promoted ${basename(foundAlternate)} → index.html in ${dir.replace(outputNorm, '') || '/'}`);
|
|
366
|
-
} catch (e) {
|
|
367
|
-
progress.log(`Error promoting ${foundAlternate} to index.html: ${e.message}`);
|
|
368
|
-
}
|
|
369
|
-
} else {
|
|
370
|
-
// Generate a simple index listing direct children
|
|
371
|
-
try {
|
|
372
|
-
const children = await readdir(dir, { withFileTypes: true });
|
|
373
|
-
|
|
374
|
-
// Filter to only include relevant files and folders
|
|
375
|
-
const filteredItems = children
|
|
376
|
-
.filter(child => {
|
|
377
|
-
// Skip hidden files and index alternates we just checked
|
|
378
|
-
if (child.name.startsWith('.')) return false;
|
|
379
|
-
if (child.name === 'index.html') return false;
|
|
380
|
-
// Skip folders config.json marks hidden — they produce no output
|
|
381
|
-
if (child.isDirectory() && isFolderSelfHidden(join(sourceDir, child.name))) return false;
|
|
382
|
-
// Include directories and html files
|
|
383
|
-
return child.isDirectory() || child.name.endsWith('.html');
|
|
384
|
-
})
|
|
385
|
-
.map(child => {
|
|
386
|
-
const isDir = child.isDirectory();
|
|
387
|
-
const baseName = isDir ? child.name : child.name.replace(/\.html$/, '');
|
|
388
|
-
return { child, isDir, baseName, ...resolveEntryNaming(isDir, baseName, sourceDir) };
|
|
389
|
-
})
|
|
390
|
-
.sort(compareEntries);
|
|
391
|
-
|
|
392
|
-
// Build items, skipping directories with no documents
|
|
393
|
-
const items = [];
|
|
394
|
-
for (const { child, isDir, label } of filteredItems) {
|
|
395
|
-
if (isDir) {
|
|
396
|
-
const childDir = join(dir, child.name);
|
|
397
|
-
if (!await directoryHasDocuments(childDir, OUTPUT_DOC_EXTENSIONS, join(sourceDir, child.name))) continue;
|
|
398
|
-
}
|
|
399
|
-
// For directories, link to /folder/index.html; for files, use the filename directly
|
|
400
|
-
const href = isDir ? `${child.name}/index.html` : child.name;
|
|
401
|
-
const icon = isDir ? '📁' : '📄';
|
|
402
|
-
items.push(`<li>${icon} <a href="${href}">${label}</a></li>`);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (items.length === 0) {
|
|
406
|
-
// Empty folder, skip generating index
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// The page's own heading and <title> follow the same naming rules, so a
|
|
411
|
-
// folder labelled "BNW - Brave New World" in the menu is not "Bnw" here.
|
|
412
|
-
const folderDisplayName = dir === outputNorm
|
|
413
|
-
? 'Home'
|
|
414
|
-
: getFolderLabel(sourceDir, getFolderConfig(sourceDir), folderName);
|
|
415
|
-
|
|
416
|
-
// Generate breadcrumbs for auto-index pages
|
|
417
|
-
const relDir = dir.replace(outputNorm, '').replace(/^\//, '');
|
|
418
|
-
const breadcrumbDir = relDir ? relDir + '/' : '/';
|
|
419
|
-
const breadcrumbHtml = generateBreadcrumbs(breadcrumbDir, 'index', null, sourceNorm);
|
|
420
|
-
|
|
421
|
-
const indexHtml = `${breadcrumbHtml}<h1>${folderDisplayName}</h1>\n<ul class="auto-index">\n${items.join('\n')}\n</ul>`;
|
|
422
|
-
|
|
423
|
-
const template = templates["default-template"];
|
|
424
|
-
if (!template) {
|
|
425
|
-
progress.log(`Warning: No default template for auto-index in ${dir}`);
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Find all style.css files up the tree and bundle them into a single CSS file
|
|
430
|
-
// (Generate mode: one CSS bundle per unique folder path)
|
|
431
|
-
let styleLink = "";
|
|
432
|
-
try {
|
|
433
|
-
const sourceDir = dir.replace(outputNorm, sourceNorm);
|
|
434
|
-
const cssPaths = await findAllStyleCss(sourceDir, sourceNorm);
|
|
435
|
-
if (cssPaths.length > 0) {
|
|
436
|
-
// Copy all CSS files to output
|
|
437
|
-
for (const cssPath of cssPaths) {
|
|
438
|
-
if (!copiedCssFiles.has(cssPath)) {
|
|
439
|
-
const cssOutputPath = cssPath.replace(sourceNorm, outputNorm);
|
|
440
|
-
const cssContent = await readFile(cssPath, 'utf8');
|
|
441
|
-
await outputFile(cssOutputPath, cssContent);
|
|
442
|
-
copiedCssFiles.add(cssPath);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
// Bundle into a single file
|
|
446
|
-
const folderRelative = sourceDir.replace(sourceNorm, '').replace(/^\//, '');
|
|
447
|
-
const bundleUrl = await bundleDocumentCss(cssPaths, outputNorm, sourceNorm, folderRelative, { minify: true });
|
|
448
|
-
styleLink = `<link rel="stylesheet" href="${bundleUrl}" />`;
|
|
449
|
-
}
|
|
450
|
-
} catch (e) {
|
|
451
|
-
// ignore CSS lookup errors
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Find all script.js files from docroot to this directory and bundle them
|
|
455
|
-
// (Generate mode: one JS bundle per unique folder path)
|
|
456
|
-
let customScript = "";
|
|
457
|
-
try {
|
|
458
|
-
const sourceDir = dir.replace(outputNorm, sourceNorm);
|
|
459
|
-
const scriptPaths = await findAllScriptJs(sourceDir, sourceNorm);
|
|
460
|
-
if (scriptPaths.length > 0) {
|
|
461
|
-
const folderRelative = sourceDir.replace(sourceNorm, '').replace(/^\//, '');
|
|
462
|
-
const bundleUrl = await bundleDocumentJs(scriptPaths, outputNorm, sourceNorm, folderRelative, { minify: true });
|
|
463
|
-
customScript = `<script src="${bundleUrl}"></script>`;
|
|
464
|
-
}
|
|
465
|
-
} catch (e) {
|
|
466
|
-
// ignore script lookup errors
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Find custom menu for this directory
|
|
470
|
-
let customMenuInfo = null;
|
|
471
|
-
if (customMenus) {
|
|
472
|
-
// Create a pseudo-file path for this directory to look up custom menu
|
|
473
|
-
const sourceDir = dir.replace(outputNorm, sourceNorm);
|
|
474
|
-
const pseudoFilePath = join(sourceDir, 'index.md'); // Pretend there's an index.md
|
|
475
|
-
customMenuInfo = getCustomMenuForFile(pseudoFilePath, sourceNorm, customMenus);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
let finalHtml = template;
|
|
479
|
-
const replacements = {
|
|
480
|
-
"${menu}": menu,
|
|
481
|
-
"${body}": indexHtml,
|
|
482
|
-
"${searchIndex}": "[]",
|
|
483
|
-
"${title}": folderDisplayName,
|
|
484
|
-
"${meta}": "{}",
|
|
485
|
-
"${transformedMetadata}": "",
|
|
486
|
-
"${styleLink}": styleLink,
|
|
487
|
-
"${customScript}": customScript,
|
|
488
|
-
"${footer}": footer
|
|
489
|
-
};
|
|
490
|
-
for (const [key, value] of Object.entries(replacements)) {
|
|
491
|
-
finalHtml = finalHtml.replace(key, value);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Add menu data attributes to body
|
|
495
|
-
if (customMenuInfo) {
|
|
496
|
-
const menuPosition = customMenuInfo.menuPosition || 'top';
|
|
497
|
-
finalHtml = finalHtml.replace(
|
|
498
|
-
/<body([^>]*)>/,
|
|
499
|
-
`<body$1 data-custom-menu="${customMenuInfo.menuJsonPath}" data-menu-position="${menuPosition}">`
|
|
500
|
-
);
|
|
501
|
-
} else {
|
|
502
|
-
// No custom menu — default to top menu
|
|
503
|
-
finalHtml = finalHtml.replace(
|
|
504
|
-
/<body([^>]*)>/,
|
|
505
|
-
`<body$1 data-menu-position="top">`
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Add cache-busting timestamps to static file references
|
|
510
|
-
finalHtml = addTimestampToHtmlStaticRefs(finalHtml, cacheBustTimestamp);
|
|
511
|
-
|
|
512
|
-
await outputFile(indexPath, finalHtml);
|
|
513
|
-
generatedCount++;
|
|
514
|
-
progress.status('Auto-index', `Generated index.html for ${dir.replace(outputNorm, '') || '/'}`);
|
|
515
|
-
} catch (e) {
|
|
516
|
-
progress.log(`Error generating auto-index for ${dir}: ${e.message}`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (generatedCount > 0 || renamedCount > 0 || skippedHtmlCount > 0) {
|
|
522
|
-
let summary = `${generatedCount} generated, ${renamedCount} promoted`;
|
|
523
|
-
if (skippedHtmlCount > 0) {
|
|
524
|
-
summary += `, ${skippedHtmlCount} skipped (existing HTML)`;
|
|
525
|
-
}
|
|
526
|
-
progress.done('Auto-index', summary);
|
|
527
|
-
} else {
|
|
528
|
-
progress.log(`Auto-index: All folders already have index.html`);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// Footer generation helpers for build
|
|
2
|
-
import { existsSync } from "
|
|
3
|
-
import { readFile } from "fs/promises";
|
|
2
|
+
import { existsSync, readFile } from "./tracedFs.js";
|
|
4
3
|
import { dirname, join, resolve } from "path";
|
|
5
4
|
import { URL } from "url";
|
|
6
5
|
import { renderFile } from "../fileRenderer.js";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Generate footer HTML from footer.md and package.json
|
|
10
10
|
* @param {string} source - resolved source path with trailing slash
|
|
11
11
|
* @param {string} _source - original source path
|
|
12
12
|
* @param {number} buildId - the current build ID
|
|
13
|
+
* @param {{now?: Date, gitHash?: string|null}} [session] - Build metadata fixed once per
|
|
14
|
+
* serve session (or generate run): the timestamp shown in the footer and the
|
|
15
|
+
* doc repo's short commit hash. Supplying them keeps the footer a pure
|
|
16
|
+
* function of its inputs for the length of the session; when omitted they are
|
|
17
|
+
* taken now.
|
|
13
18
|
* @returns {Promise<string>} Footer HTML
|
|
14
19
|
*/
|
|
15
|
-
export async function getFooter(source, _source, buildId) {
|
|
20
|
+
export async function getFooter(source, _source, buildId, session = {}) {
|
|
16
21
|
const footerParts = [];
|
|
17
22
|
|
|
18
23
|
// Try to read footer.md from source root
|
|
@@ -40,7 +45,6 @@ export async function getFooter(source, _source, buildId) {
|
|
|
40
45
|
if (existsSync(packagePath)) {
|
|
41
46
|
const packageJson = await readFile(packagePath, 'utf8');
|
|
42
47
|
docPackage = JSON.parse(packageJson);
|
|
43
|
-
console.log(`Found doc package.json at ${packagePath}`);
|
|
44
48
|
break;
|
|
45
49
|
}
|
|
46
50
|
} catch (e) {
|
|
@@ -61,7 +65,6 @@ export async function getFooter(source, _source, buildId) {
|
|
|
61
65
|
const ursaPackageJson = await readFile(ursaPackagePath, 'utf8');
|
|
62
66
|
const ursaPackage = JSON.parse(ursaPackageJson);
|
|
63
67
|
ursaVersion = ursaPackage.version;
|
|
64
|
-
console.log(`Found ursa package.json at ${ursaPackagePath}, version: ${ursaVersion}`);
|
|
65
68
|
}
|
|
66
69
|
} catch (e) {
|
|
67
70
|
console.error(`Error reading ursa package.json: ${e.message}`);
|
|
@@ -75,7 +78,7 @@ export async function getFooter(source, _source, buildId) {
|
|
|
75
78
|
metaParts.push(`build ${buildId}`);
|
|
76
79
|
|
|
77
80
|
// Full date/time in a readable format
|
|
78
|
-
const now = new Date();
|
|
81
|
+
const now = session.now ?? new Date();
|
|
79
82
|
const timestamp = now.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
|
80
83
|
metaParts.push(timestamp);
|
|
81
84
|
|
|
@@ -94,20 +97,30 @@ export async function getFooter(source, _source, buildId) {
|
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
//
|
|
100
|
+
// Git short hash of the doc repo (as HTML comment)
|
|
101
|
+
const gitHash = session.gitHash !== undefined ? session.gitHash : readGitHash(_source);
|
|
102
|
+
if (gitHash) {
|
|
103
|
+
footerParts.push(`<!-- git: ${gitHash} -->`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return footerParts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The doc repo's short commit hash, or null outside a git work tree.
|
|
111
|
+
* @param {string} _source - Source directory
|
|
112
|
+
* @returns {string|null}
|
|
113
|
+
*/
|
|
114
|
+
export function readGitHash(_source) {
|
|
98
115
|
try {
|
|
99
|
-
const { execSync } = await import('child_process');
|
|
100
116
|
const gitHash = execSync('git rev-parse --short HEAD', {
|
|
101
117
|
cwd: resolve(_source),
|
|
102
118
|
encoding: 'utf8',
|
|
103
119
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
104
120
|
}).trim();
|
|
105
|
-
|
|
106
|
-
footerParts.push(`<!-- git: ${gitHash} -->`);
|
|
107
|
-
}
|
|
121
|
+
return gitHash || null;
|
|
108
122
|
} catch (e) {
|
|
109
|
-
// Not a git repo or git not available
|
|
123
|
+
// Not a git repo or git not available
|
|
124
|
+
return null;
|
|
110
125
|
}
|
|
111
|
-
|
|
112
|
-
return footerParts.join('\n');
|
|
113
126
|
}
|