@kenjura/ursa 0.53.0 → 0.55.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 +13 -1
- package/meta/default.css +44 -9
- package/meta/menu.js +237 -2
- package/meta/search.js +41 -0
- package/package.json +1 -1
- package/src/helper/build/autoIndex.js +197 -0
- package/src/helper/build/batch.js +19 -0
- package/src/helper/build/cacheBust.js +62 -0
- package/src/helper/build/excludeFilter.js +67 -0
- package/src/helper/build/footer.js +113 -0
- package/src/helper/build/index.js +13 -0
- package/src/helper/build/menu.js +103 -0
- package/src/helper/build/metadata.js +30 -0
- package/src/helper/build/pathUtils.js +13 -0
- package/src/helper/build/progress.js +35 -0
- package/src/helper/build/templates.js +30 -0
- package/src/helper/build/titleCase.js +7 -0
- package/src/helper/build/watchCache.js +26 -0
- package/src/helper/customMenu.js +303 -0
- package/src/jobs/generate.js +97 -561
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Exclude/filter helpers for build
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile, stat } from "fs/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse exclude option - can be comma-separated paths or a file path
|
|
7
|
+
* @param {string} excludeOption - The exclude option value
|
|
8
|
+
* @param {string} source - Source directory path
|
|
9
|
+
* @returns {Promise<Set<string>>} Set of excluded folder paths (normalized)
|
|
10
|
+
*/
|
|
11
|
+
export async function parseExcludeOption(excludeOption, source) {
|
|
12
|
+
const excludedPaths = new Set();
|
|
13
|
+
|
|
14
|
+
if (!excludeOption) return excludedPaths;
|
|
15
|
+
|
|
16
|
+
// Check if it's a file path (exists as a file)
|
|
17
|
+
const isFile = existsSync(excludeOption) && (await stat(excludeOption)).isFile();
|
|
18
|
+
|
|
19
|
+
let patterns;
|
|
20
|
+
if (isFile) {
|
|
21
|
+
// Read patterns from file (one per line)
|
|
22
|
+
const content = await readFile(excludeOption, 'utf8');
|
|
23
|
+
patterns = content.split('\n')
|
|
24
|
+
.map(line => line.trim())
|
|
25
|
+
.filter(line => line && !line.startsWith('#')); // Skip empty lines and comments
|
|
26
|
+
} else {
|
|
27
|
+
// Treat as comma-separated list
|
|
28
|
+
patterns = excludeOption.split(',').map(p => p.trim()).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Normalize patterns to absolute paths
|
|
32
|
+
for (const pattern of patterns) {
|
|
33
|
+
// Remove leading/trailing slashes and normalize
|
|
34
|
+
const normalized = pattern.replace(/^\/+|\/+$/g, '');
|
|
35
|
+
// Store as relative path for easier matching
|
|
36
|
+
excludedPaths.add(normalized);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return excludedPaths;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a filter function that excludes files in specified folders
|
|
44
|
+
* @param {Set<string>} excludedPaths - Set of excluded folder paths
|
|
45
|
+
* @param {string} source - Source directory path
|
|
46
|
+
* @returns {Function} Filter function
|
|
47
|
+
*/
|
|
48
|
+
export function createExcludeFilter(excludedPaths, source) {
|
|
49
|
+
if (excludedPaths.size === 0) {
|
|
50
|
+
return () => true; // No exclusions, allow all
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (filePath) => {
|
|
54
|
+
// Get path relative to source
|
|
55
|
+
const relativePath = filePath.replace(source, '').replace(/^\/+/, '');
|
|
56
|
+
|
|
57
|
+
// Check if file is in any excluded folder
|
|
58
|
+
for (const excluded of excludedPaths) {
|
|
59
|
+
if (relativePath === excluded ||
|
|
60
|
+
relativePath.startsWith(excluded + '/') ||
|
|
61
|
+
relativePath.startsWith(excluded + '\\')) {
|
|
62
|
+
return false; // Exclude this file
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return true; // Include this file
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Footer generation helpers for build
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
import { URL } from "url";
|
|
6
|
+
import { renderFile } from "../fileRenderer.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate footer HTML from footer.md and package.json
|
|
10
|
+
* @param {string} source - resolved source path with trailing slash
|
|
11
|
+
* @param {string} _source - original source path
|
|
12
|
+
* @param {number} buildId - the current build ID
|
|
13
|
+
* @returns {Promise<string>} Footer HTML
|
|
14
|
+
*/
|
|
15
|
+
export async function getFooter(source, _source, buildId) {
|
|
16
|
+
const footerParts = [];
|
|
17
|
+
|
|
18
|
+
// Try to read footer.md from source root
|
|
19
|
+
const footerPath = join(source, 'footer.md');
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(footerPath)) {
|
|
22
|
+
const footerMd = await readFile(footerPath, 'utf8');
|
|
23
|
+
const footerHtml = renderFile({ fileContents: footerMd, type: '.md' });
|
|
24
|
+
footerParts.push(`<div class="footer-content">${footerHtml}</div>`);
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error(`Error reading footer.md: ${e.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Try to read package.json from doc repo (check both source dir and parent)
|
|
31
|
+
let docPackage = null;
|
|
32
|
+
const sourceDir = resolve(_source);
|
|
33
|
+
const packagePaths = [
|
|
34
|
+
join(sourceDir, 'package.json'), // In source dir itself
|
|
35
|
+
join(sourceDir, '..', 'package.json'), // One level up (if docs is a subfolder)
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const packagePath of packagePaths) {
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(packagePath)) {
|
|
41
|
+
const packageJson = await readFile(packagePath, 'utf8');
|
|
42
|
+
docPackage = JSON.parse(packageJson);
|
|
43
|
+
console.log(`Found doc package.json at ${packagePath}`);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// Continue to next path
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get ursa version from ursa's own package.json
|
|
52
|
+
// Use import.meta.url to find the package.json relative to this file
|
|
53
|
+
let ursaVersion = 'unknown';
|
|
54
|
+
try {
|
|
55
|
+
// From src/helper/build/footer.js, go up to package root
|
|
56
|
+
const currentFileUrl = new URL(import.meta.url);
|
|
57
|
+
const currentDir = dirname(currentFileUrl.pathname);
|
|
58
|
+
const ursaPackagePath = resolve(currentDir, '..', '..', '..', 'package.json');
|
|
59
|
+
|
|
60
|
+
if (existsSync(ursaPackagePath)) {
|
|
61
|
+
const ursaPackageJson = await readFile(ursaPackagePath, 'utf8');
|
|
62
|
+
const ursaPackage = JSON.parse(ursaPackageJson);
|
|
63
|
+
ursaVersion = ursaPackage.version;
|
|
64
|
+
console.log(`Found ursa package.json at ${ursaPackagePath}, version: ${ursaVersion}`);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(`Error reading ursa package.json: ${e.message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build meta line: version, build id, timestamp, "generated by ursa"
|
|
71
|
+
const metaParts = [];
|
|
72
|
+
if (docPackage?.version) {
|
|
73
|
+
metaParts.push(`v${docPackage.version}`);
|
|
74
|
+
}
|
|
75
|
+
metaParts.push(`build ${buildId}`);
|
|
76
|
+
|
|
77
|
+
// Full date/time in a readable format
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const timestamp = now.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
|
80
|
+
metaParts.push(timestamp);
|
|
81
|
+
|
|
82
|
+
metaParts.push(`Generated by <a href="https://www.npmjs.com/package/@kenjura/ursa">ursa</a> v${ursaVersion}`);
|
|
83
|
+
|
|
84
|
+
footerParts.push(`<div class="footer-meta">${metaParts.join(' • ')}</div>`);
|
|
85
|
+
|
|
86
|
+
// Copyright line from doc package.json
|
|
87
|
+
if (docPackage?.copyright) {
|
|
88
|
+
footerParts.push(`<div class="footer-copyright">${docPackage.copyright}</div>`);
|
|
89
|
+
} else if (docPackage?.author) {
|
|
90
|
+
const year = new Date().getFullYear();
|
|
91
|
+
const author = typeof docPackage.author === 'string' ? docPackage.author : docPackage.author.name;
|
|
92
|
+
if (author) {
|
|
93
|
+
footerParts.push(`<div class="footer-copyright">© ${year} ${author}</div>`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try to get git short hash of doc repo (as HTML comment)
|
|
98
|
+
try {
|
|
99
|
+
const { execSync } = await import('child_process');
|
|
100
|
+
const gitHash = execSync('git rev-parse --short HEAD', {
|
|
101
|
+
cwd: resolve(_source),
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
104
|
+
}).trim();
|
|
105
|
+
if (gitHash) {
|
|
106
|
+
footerParts.push(`<!-- git: ${gitHash} -->`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Not a git repo or git not available - silently skip
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return footerParts.join('\n');
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Barrel file for build helpers
|
|
2
|
+
export * from './cacheBust.js';
|
|
3
|
+
export * from './batch.js';
|
|
4
|
+
export * from './progress.js';
|
|
5
|
+
export * from './watchCache.js';
|
|
6
|
+
export * from './titleCase.js';
|
|
7
|
+
export * from './excludeFilter.js';
|
|
8
|
+
export * from './pathUtils.js';
|
|
9
|
+
export * from './templates.js';
|
|
10
|
+
export * from './menu.js';
|
|
11
|
+
export * from './metadata.js';
|
|
12
|
+
export * from './footer.js';
|
|
13
|
+
export * from './autoIndex.js';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Menu helpers for build
|
|
2
|
+
import { getAutomenu } from "../automenu.js";
|
|
3
|
+
import { renderFile } from "../fileRenderer.js";
|
|
4
|
+
import { findCustomMenu, parseCustomMenu, buildCustomMenuHtml } from "../customMenu.js";
|
|
5
|
+
import { dirname, relative, resolve } from "path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get menu HTML and menu data from source directory
|
|
9
|
+
* @param {string[]} allSourceFilenames - All source file names
|
|
10
|
+
* @param {string} source - Source directory path
|
|
11
|
+
* @param {Set<string>} validPaths - Set of valid internal paths for link validation
|
|
12
|
+
* @returns {Promise<{html: string, menuData: Object}>} Menu HTML and menu data
|
|
13
|
+
*/
|
|
14
|
+
export async function getMenu(allSourceFilenames, source, validPaths) {
|
|
15
|
+
const menuResult = await getAutomenu(source, validPaths);
|
|
16
|
+
const menuBody = renderFile({ fileContents: menuResult.html, type: ".md" });
|
|
17
|
+
return {
|
|
18
|
+
html: menuBody,
|
|
19
|
+
menuData: menuResult.menuData
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find all unique custom menus in the source tree
|
|
25
|
+
* @param {string[]} allSourceFilenames - All source file names
|
|
26
|
+
* @param {string} source - Source directory path
|
|
27
|
+
* @returns {Map<string, {menuPath: string, menuDir: string, menuData: Array}>} Map of menu dir to menu info
|
|
28
|
+
*/
|
|
29
|
+
export function findAllCustomMenus(allSourceFilenames, source) {
|
|
30
|
+
const customMenus = new Map();
|
|
31
|
+
const checkedDirs = new Set();
|
|
32
|
+
|
|
33
|
+
// Check each directory for custom menus
|
|
34
|
+
for (const file of allSourceFilenames) {
|
|
35
|
+
const dir = dirname(file);
|
|
36
|
+
if (checkedDirs.has(dir)) continue;
|
|
37
|
+
checkedDirs.add(dir);
|
|
38
|
+
|
|
39
|
+
const menuInfo = findCustomMenu(dir, source);
|
|
40
|
+
if (menuInfo && !customMenus.has(menuInfo.menuDir)) {
|
|
41
|
+
const menuData = parseCustomMenu(menuInfo.content, menuInfo.menuDir, source);
|
|
42
|
+
customMenus.set(menuInfo.menuDir, {
|
|
43
|
+
menuPath: menuInfo.path,
|
|
44
|
+
menuDir: menuInfo.menuDir,
|
|
45
|
+
menuData,
|
|
46
|
+
// The URL path for the menu JSON file
|
|
47
|
+
menuJsonPath: '/public/custom-menu-' + getMenuId(menuInfo.menuDir, source) + '.json',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return customMenus;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a unique ID for a custom menu based on its directory
|
|
57
|
+
* @param {string} menuDir - The directory where the menu is located
|
|
58
|
+
* @param {string} source - The source root directory
|
|
59
|
+
* @returns {string} - A URL-safe ID
|
|
60
|
+
*/
|
|
61
|
+
function getMenuId(menuDir, source) {
|
|
62
|
+
const relativePath = relative(source, menuDir);
|
|
63
|
+
if (!relativePath) return 'root';
|
|
64
|
+
return relativePath.replace(/[\/\\]/g, '-').replace(/[^a-zA-Z0-9-]/g, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the custom menu info for a specific file path
|
|
69
|
+
* @param {string} filePath - The source file path
|
|
70
|
+
* @param {string} source - The source root directory
|
|
71
|
+
* @param {Map} customMenus - Map of all custom menus
|
|
72
|
+
* @returns {{menuJsonPath: string, menuDir: string} | null} - Custom menu info or null
|
|
73
|
+
*/
|
|
74
|
+
export function getCustomMenuForFile(filePath, source, customMenus) {
|
|
75
|
+
const fileDir = resolve(dirname(filePath));
|
|
76
|
+
const sourceResolved = resolve(source);
|
|
77
|
+
|
|
78
|
+
// Walk up from file dir to find matching custom menu
|
|
79
|
+
let currentDir = fileDir;
|
|
80
|
+
while (currentDir.startsWith(sourceResolved)) {
|
|
81
|
+
if (customMenus.has(currentDir)) {
|
|
82
|
+
const menuInfo = customMenus.get(currentDir);
|
|
83
|
+
return {
|
|
84
|
+
menuJsonPath: menuInfo.menuJsonPath,
|
|
85
|
+
menuDir: relative(source, menuInfo.menuDir) || '',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const parentDir = dirname(currentDir);
|
|
89
|
+
if (parentDir === currentDir) break;
|
|
90
|
+
currentDir = parentDir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build HTML for a custom menu
|
|
98
|
+
* @param {Array} menuData - The parsed menu data
|
|
99
|
+
* @returns {string} - HTML string for the menu
|
|
100
|
+
*/
|
|
101
|
+
export function buildCustomMenuHtmlExport(menuData) {
|
|
102
|
+
return buildCustomMenuHtml(menuData);
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Metadata transformation helpers for build
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get transformed metadata using custom or default transform function
|
|
6
|
+
* @param {string} dirname - Directory containing the file
|
|
7
|
+
* @param {Object} metadata - Raw metadata object
|
|
8
|
+
* @returns {Promise<string>} Transformed metadata string
|
|
9
|
+
*/
|
|
10
|
+
export async function getTransformedMetadata(dirname, metadata) {
|
|
11
|
+
// custom transform? else, use default
|
|
12
|
+
const customTransformFnFilename = join(dirname, "transformMetadata.js");
|
|
13
|
+
let transformFn = defaultTransformFn;
|
|
14
|
+
try {
|
|
15
|
+
const customTransformFn = (await import(customTransformFnFilename)).default;
|
|
16
|
+
if (typeof customTransformFn === "function")
|
|
17
|
+
transformFn = customTransformFn;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// No custom transform found, use default
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return transformFn(metadata);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return "error transforming metadata";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultTransformFn(metadata) {
|
|
28
|
+
return "default transform";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Path utility helpers for build
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add trailing slash to a path if not present
|
|
5
|
+
* @param {string} somePath - Path to modify
|
|
6
|
+
* @returns {string} Path with trailing slash
|
|
7
|
+
*/
|
|
8
|
+
export function addTrailingSlash(somePath) {
|
|
9
|
+
if (typeof somePath !== "string") return somePath;
|
|
10
|
+
if (somePath.length < 1) return somePath;
|
|
11
|
+
if (somePath[somePath.length - 1] === "/") return somePath;
|
|
12
|
+
return `${somePath}/`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Progress reporter for build
|
|
2
|
+
|
|
3
|
+
export class ProgressReporter {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.lines = {};
|
|
6
|
+
this.isTTY = process.stdout.isTTY;
|
|
7
|
+
}
|
|
8
|
+
status(name, message) {
|
|
9
|
+
if (this.isTTY) {
|
|
10
|
+
const line = `${name}: ${message}`;
|
|
11
|
+
this.lines[name] = line;
|
|
12
|
+
process.stdout.write(`\r\x1b[K${line}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
done(name, message) {
|
|
16
|
+
if (this.isTTY) {
|
|
17
|
+
process.stdout.write(`\r\x1b[K${name}: ${message}\n`);
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`${name}: ${message}`);
|
|
20
|
+
}
|
|
21
|
+
delete this.lines[name];
|
|
22
|
+
}
|
|
23
|
+
log(message) {
|
|
24
|
+
if (this.isTTY) {
|
|
25
|
+
process.stdout.write(`\r\x1b[K${message}\n`);
|
|
26
|
+
} else {
|
|
27
|
+
console.log(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
clear() {
|
|
31
|
+
if (this.isTTY) {
|
|
32
|
+
process.stdout.write(`\r\x1b[K`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Template helpers for build
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { parse } from "path";
|
|
4
|
+
import { recurse } from "../recursive-readdir.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get all templates from meta directory
|
|
8
|
+
* @param {string} meta - Full path to meta files directory
|
|
9
|
+
* @returns {Promise<Object>} Map of templateName to templateBody
|
|
10
|
+
*/
|
|
11
|
+
export async function getTemplates(meta) {
|
|
12
|
+
const allMetaFilenames = await recurse(meta);
|
|
13
|
+
const allHtmlFilenames = allMetaFilenames.filter((filename) =>
|
|
14
|
+
filename.match(/\.html/)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
let templates = {};
|
|
18
|
+
const templatesArray = await Promise.all(
|
|
19
|
+
allHtmlFilenames.map(async (filename) => {
|
|
20
|
+
const { name } = parse(filename);
|
|
21
|
+
const fileContent = await readFile(filename, "utf8");
|
|
22
|
+
return [name, fileContent];
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
templatesArray.forEach(
|
|
26
|
+
([templateName, templateText]) => (templates[templateName] = templateText)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return templates;
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Watch mode cache and clear function for build
|
|
2
|
+
|
|
3
|
+
export const watchModeCache = {
|
|
4
|
+
templates: null,
|
|
5
|
+
menu: null,
|
|
6
|
+
footer: null,
|
|
7
|
+
validPaths: null,
|
|
8
|
+
source: null,
|
|
9
|
+
meta: null,
|
|
10
|
+
output: null,
|
|
11
|
+
hashCache: null,
|
|
12
|
+
cacheBustTimestamp: null,
|
|
13
|
+
lastFullBuild: 0,
|
|
14
|
+
isInitialized: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function clearWatchCache(cssPathCache) {
|
|
18
|
+
watchModeCache.templates = null;
|
|
19
|
+
watchModeCache.menu = null;
|
|
20
|
+
watchModeCache.footer = null;
|
|
21
|
+
watchModeCache.validPaths = null;
|
|
22
|
+
watchModeCache.hashCache = null;
|
|
23
|
+
watchModeCache.isInitialized = false;
|
|
24
|
+
if (cssPathCache) cssPathCache.clear();
|
|
25
|
+
console.log('Watch cache cleared');
|
|
26
|
+
}
|