@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/meta/default.css +149 -3
  3. package/meta/templates/default-template/default.css +1268 -0
  4. package/meta/{default-template.html → templates/default-template/index.html} +49 -2
  5. package/meta/{menu.js → templates/default-template/menu.js} +1 -1
  6. package/meta/templates/default-template/sectionify.js +46 -0
  7. package/meta/templates/default-template/widgets.js +701 -0
  8. package/package.json +1 -1
  9. package/src/dev.js +125 -34
  10. package/src/helper/assetBundler.js +471 -0
  11. package/src/helper/build/autoIndex.js +26 -23
  12. package/src/helper/build/cacheBust.js +79 -0
  13. package/src/helper/build/navCache.js +4 -0
  14. package/src/helper/build/templates.js +176 -19
  15. package/src/helper/build/watchCache.js +7 -0
  16. package/src/helper/customMenu.js +4 -2
  17. package/src/helper/dependencyTracker.js +269 -0
  18. package/src/helper/findScriptJs.js +29 -0
  19. package/src/helper/findStyleCss.js +29 -0
  20. package/src/helper/portUtils.js +132 -0
  21. package/src/jobs/generate.js +276 -59
  22. package/src/serve.js +446 -162
  23. package/meta/character-sheet.css +0 -50
  24. package/meta/widgets.js +0 -376
  25. /package/meta/{goudy_bookletter_1911-webfont.woff → shared/goudy_bookletter_1911-webfont.woff} +0 -0
  26. /package/meta/{character-sheet/css → templates/character-sheet-template}/character-sheet.css +0 -0
  27. /package/meta/{character-sheet/js → templates/character-sheet-template}/components.js +0 -0
  28. /package/meta/{cssui.bundle.min.css → templates/character-sheet-template/cssui.bundle.min.css} +0 -0
  29. /package/meta/{character-sheet-template.html → templates/character-sheet-template/index.html} +0 -0
  30. /package/meta/{character-sheet/js → templates/character-sheet-template}/main.js +0 -0
  31. /package/meta/{character-sheet/js → templates/character-sheet-template}/model.js +0 -0
  32. /package/meta/{search.js → templates/default-template/search.js} +0 -0
  33. /package/meta/{sticky.js → templates/default-template/sticky.js} +0 -0
  34. /package/meta/{toc-generator.js → templates/default-template/toc-generator.js} +0 -0
  35. /package/meta/{toc.js → templates/default-template/toc.js} +0 -0
  36. /package/meta/{template2.html → templates/template2/index.html} +0 -0
package/src/dev.js CHANGED
@@ -20,7 +20,7 @@ const { readFile, readdir, stat, mkdir } = promises;
20
20
  // Import helper modules
21
21
  import { renderFileAsync } from "./helper/fileRenderer.js";
22
22
  import { findStyleCss } from "./helper/findStyleCss.js";
23
- import { findScriptJs } from "./helper/findScriptJs.js";
23
+ import { findAllScriptJs } from "./helper/findScriptJs.js";
24
24
  import { extractMetadata, getAutoIndexConfig, isMetadataOnly } from "./helper/metadataExtractor.js";
25
25
  import { injectFrontmatterTable } from "./helper/frontmatterTable.js";
26
26
  import { buildValidPaths, markInactiveLinks, resolveRelativeUrls } from "./helper/linkValidator.js";
@@ -33,9 +33,12 @@ import { extractImageReferences } from "./helper/imageExtractor.js";
33
33
  import { recurse } from "./helper/recursive-readdir.js";
34
34
  import { isFolderHidden, clearConfigCache } from "./helper/folderConfig.js";
35
35
  import { extractSections } from "./helper/sectionExtractor.js";
36
- import { getTemplates, getMenu, findAllCustomMenus, getCustomMenuForFile, getTransformedMetadata, getFooter, toTitleCase, addTrailingSlash, generateAutoIndexHtmlFromSource } from "./helper/build/index.js";
36
+ import { getTemplates, getMenu, findAllCustomMenus, getCustomMenuForFile, getTransformedMetadata, getFooter, toTitleCase, addTrailingSlash, generateAutoIndexHtmlFromSource, copyMetaAssets } from "./helper/build/index.js";
37
37
  import { findCustomMenu, extractMenuFrontmatter, parseCustomMenu, combineAutoAndManualMenu } from "./helper/customMenu.js";
38
38
  import { getAndIncrementBuildId } from "./helper/ursaConfig.js";
39
+ import { resolvePort } from "./helper/portUtils.js";
40
+ import { findAllStyleCss } from "./helper/findStyleCss.js";
41
+ import { bundleMetaTemplateAssets, generateSeparateCssTags, generateSeparateJsTags, clearMetaBundleCache } from "./helper/assetBundler.js";
39
42
 
40
43
  // Dev mode state
41
44
  const devState = {
@@ -58,6 +61,7 @@ const devState = {
58
61
  footer: null,
59
62
  fullTextIndex: null,
60
63
  searchIndex: null,
64
+ recentActivity: null,
61
65
 
62
66
  // Path → nearest menu.md mapping
63
67
  menuPathMap: new Map(),
@@ -265,6 +269,23 @@ function findNearestMenu(dirPath) {
265
269
  return null;
266
270
  }
267
271
 
272
+ /**
273
+ * Find all style.css files from docroot to dirPath (all levels)
274
+ */
275
+ async function findAllStyles(dirPath) {
276
+ const { stylePathMap, source } = devState;
277
+
278
+ // Use a separate cache key to avoid collision with findNearestStyle
279
+ const cacheKey = `all:${dirPath}`;
280
+ if (stylePathMap.has(cacheKey)) {
281
+ return stylePathMap.get(cacheKey);
282
+ }
283
+
284
+ const stylePaths = await findAllStyleCss(dirPath, source);
285
+ stylePathMap.set(cacheKey, stylePaths);
286
+ return stylePaths;
287
+ }
288
+
268
289
  /**
269
290
  * Find the nearest style.css
270
291
  */
@@ -282,19 +303,19 @@ async function findNearestStyle(dirPath) {
282
303
  }
283
304
 
284
305
  /**
285
- * Find the nearest script.js
306
+ * Find all script.js files from docroot to dirPath
286
307
  */
287
- async function findNearestScript(dirPath) {
288
- const { scriptPathMap } = devState;
308
+ async function findAllScripts(dirPath) {
309
+ const { scriptPathMap, source } = devState;
289
310
 
290
311
  // Check cache first
291
312
  if (scriptPathMap.has(dirPath)) {
292
313
  return scriptPathMap.get(dirPath);
293
314
  }
294
315
 
295
- const scriptPath = await findScriptJs(dirPath);
296
- scriptPathMap.set(dirPath, scriptPath);
297
- return scriptPath;
316
+ const scriptPaths = await findAllScriptJs(dirPath, source);
317
+ scriptPathMap.set(dirPath, scriptPaths);
318
+ return scriptPaths;
298
319
  }
299
320
 
300
321
  /**
@@ -439,32 +460,39 @@ async function wrapInTemplate(body, title, fileMeta, urlPath, sourcePath) {
439
460
  throw new Error(`Template not found: ${requestedTemplateName || 'default-template'}`);
440
461
  }
441
462
 
442
- // Find nearest style.css
463
+ // Find all style.css files from docroot to current dir and serve as separate tags
464
+ // (Serve/dev mode: separate tags per level for individual invalidation)
443
465
  let styleLink = "";
444
466
  try {
445
467
  const styleDir = sourcePath ? dirname(sourcePath) : source;
446
- const cssPath = await findNearestStyle(styleDir);
447
- if (cssPath) {
448
- const cssUrlPath = '/' + cssPath.replace(source, '');
449
- styleLink = `<link rel="stylesheet" href="${cssUrlPath}" />`;
450
-
451
- // Copy CSS file to output
452
- const cssOutputPath = cssPath.replace(source, output);
453
- const cssContent = await readFile(cssPath, 'utf8');
454
- await outputFile(cssOutputPath, cssContent);
468
+ const cssPaths = await findAllStyles(styleDir);
469
+ if (cssPaths.length > 0) {
470
+ // Copy all CSS files to output
471
+ for (const cssPath of cssPaths) {
472
+ const cssOutputPath = cssPath.replace(source, output);
473
+ const cssContent = await readFile(cssPath, 'utf8');
474
+ await outputFile(cssOutputPath, cssContent);
475
+ }
476
+ styleLink = generateSeparateCssTags(cssPaths, source);
455
477
  }
456
478
  } catch (e) {
457
479
  // Ignore CSS errors
458
480
  }
459
481
 
460
- // Find nearest script.js and inline its contents
482
+ // Find all script.js files from docroot to current dir and serve as separate external tags
483
+ // (Serve/dev mode: separate tags per level for individual invalidation)
461
484
  let customScript = "";
462
485
  try {
463
486
  const scriptDir = sourcePath ? dirname(sourcePath) : source;
464
- const scriptPath = await findNearestScript(scriptDir);
465
- if (scriptPath) {
466
- const scriptContent = await readFile(scriptPath, 'utf8');
467
- customScript = `<script>\n${scriptContent}\n</script>`;
487
+ const scriptPaths = await findAllScripts(scriptDir);
488
+ if (scriptPaths.length > 0) {
489
+ // Copy all script files to output so they can be served
490
+ for (const scriptPath of scriptPaths) {
491
+ const scriptOutputPath = scriptPath.replace(source, output);
492
+ const scriptContent = await readFile(scriptPath, 'utf8');
493
+ await outputFile(scriptOutputPath, scriptContent);
494
+ }
495
+ customScript = generateSeparateJsTags(scriptPaths, source);
468
496
  }
469
497
  } catch (e) {
470
498
  // Ignore script errors
@@ -614,9 +642,10 @@ async function buildBackgroundCaches() {
614
642
  await findNearestStyle(dir);
615
643
  }
616
644
 
617
- // 7. Pre-load templates
645
+ // 7. Pre-load templates and re-bundle meta assets
618
646
  console.log(' 📄 Pre-loading templates...');
619
- devState.templates = await getTemplates(meta);
647
+ const rawTemplatesBg = await getTemplates(meta);
648
+ devState.templates = await bundleMetaTemplateAssets(rawTemplatesBg, meta, resolve(output, 'public'), { minify: true, sourcemap: false });
620
649
 
621
650
  // 8. Build footer
622
651
  console.log(' 📝 Building footer...');
@@ -635,6 +664,7 @@ async function buildBackgroundCaches() {
635
664
 
636
665
  const searchIndex = [];
637
666
  const fullTextDocs = [];
667
+ const recentActivity = [];
638
668
 
639
669
  for (const article of allArticles) {
640
670
  try {
@@ -657,6 +687,16 @@ async function buildBackgroundCaches() {
657
687
  title,
658
688
  content
659
689
  });
690
+
691
+ // Collect mtime for recent activity
692
+ try {
693
+ const articleStat = await stat(article);
694
+ recentActivity.push({
695
+ title,
696
+ url: relativePath.startsWith('/') ? relativePath : '/' + relativePath,
697
+ mtime: articleStat.mtimeMs
698
+ });
699
+ } catch (e) {}
660
700
  } catch (e) {}
661
701
  }
662
702
 
@@ -664,6 +704,10 @@ async function buildBackgroundCaches() {
664
704
  devState.fullTextIndex = buildFullTextIndex(fullTextDocs);
665
705
  devState.searchIndex = searchIndex;
666
706
 
707
+ // Build recent activity (top 10 by mtime)
708
+ recentActivity.sort((a, b) => b.mtime - a.mtime);
709
+ devState.recentActivity = recentActivity.slice(0, 10);
710
+
667
711
  // Write search index files
668
712
  const publicDir = join(output, 'public');
669
713
  await mkdir(publicDir, { recursive: true });
@@ -671,6 +715,7 @@ async function buildBackgroundCaches() {
671
715
  await outputFile(join(publicDir, 'search-index.json'), JSON.stringify(searchIndex));
672
716
  await outputFile(join(publicDir, 'fulltext-index.json'), JSON.stringify(devState.fullTextIndex));
673
717
  await outputFile(join(publicDir, 'menu-data.json'), JSON.stringify(devState.menuData));
718
+ await outputFile(join(publicDir, 'recent-activity.json'), JSON.stringify(devState.recentActivity));
674
719
 
675
720
  devState.searchReady = true;
676
721
  console.log('✅ Search index ready');
@@ -705,15 +750,20 @@ export async function dev({
705
750
  console.log(`🎨 Meta: ${metaDir}`);
706
751
  console.log(`📤 Output: ${outputDir}`);
707
752
  console.log('━'.repeat(50));
708
-
753
+
754
+ // Resolve port (prompt user if occupied)
755
+ port = await resolvePort(port);
756
+
709
757
  // Create output directory and copy meta files
710
758
  await mkdir(outputDir, { recursive: true });
711
759
  const publicDir = join(outputDir, 'public');
712
760
  await mkdir(publicDir, { recursive: true });
713
- await copyDir(metaDir, publicDir);
761
+ await copyMetaAssets(metaDir, publicDir);
714
762
 
715
- // Pre-load templates for immediate use
716
- devState.templates = await getTemplates(metaDir);
763
+ // Pre-load templates and bundle meta assets (minify without obfuscation for debuggability)
764
+ let rawTemplates = await getTemplates(metaDir);
765
+ devState.templates = await bundleMetaTemplateAssets(rawTemplates, metaDir, publicDir, { minify: true, sourcemap: false });
766
+ console.log('📦 Meta template assets bundled');
717
767
 
718
768
  // Start server immediately
719
769
  const app = express();
@@ -931,20 +981,29 @@ export async function dev({
931
981
  devState.menuReady = false;
932
982
  await buildBackgroundCaches();
933
983
  } else if (isCssChange) {
934
- // Copy CSS to output
984
+ // Copy CSS to output and clear style cache so next render picks up changes
985
+ devState.stylePathMap.clear();
935
986
  try {
936
987
  const relativePath = name.replace(sourceDir, '');
937
988
  const outputPath = join(outputDir, relativePath);
938
989
  const content = await readFile(name, 'utf8');
939
990
  await outputFile(outputPath, content);
940
- console.log(`✅ Copied ${relativePath}`);
991
+ console.log(`✅ Copied ${relativePath} (style cache cleared)`);
941
992
  } catch (e) {
942
993
  console.error(`Error copying CSS: ${e.message}`);
943
994
  }
944
995
  } else if (isScriptChange) {
945
- // Clear script cache so next render picks up changes
996
+ // Copy script to output and clear script cache so next render picks up changes
946
997
  devState.scriptPathMap.clear();
947
- console.log(`✅ Script cache cleared for ${name}`);
998
+ try {
999
+ const relativePath = name.replace(sourceDir, '');
1000
+ const outputPath = join(outputDir, relativePath);
1001
+ const content = await readFile(name, 'utf8');
1002
+ await outputFile(outputPath, content);
1003
+ console.log(`✅ Copied ${relativePath} (script cache cleared)`);
1004
+ } catch (e) {
1005
+ console.error(`Error copying script: ${e.message}`);
1006
+ }
948
1007
  } else if (isComponentChange) {
949
1008
  // Component files (.tsx, .jsx, .ts) may be imported by MDX files
950
1009
  // Clear all MDX document caches so they re-compile with the updated component
@@ -956,6 +1015,33 @@ export async function dev({
956
1015
  console.log(`✅ MDX caches cleared for component change: ${name}`);
957
1016
  }
958
1017
 
1018
+ // Update recent activity for article changes
1019
+ const isArticleChange = name && /\.(md|mdx|txt|yml)$/.test(name);
1020
+ if (isArticleChange && devState.recentActivity) {
1021
+ try {
1022
+ const articleStat = await stat(name);
1023
+ const ext = extname(name);
1024
+ const base = basename(name, ext);
1025
+ const relativePath = name.replace(sourceDir + '/', '').replace(/\.(md|mdx|txt|yml)$/, '.html');
1026
+ const titleBase = (base === 'index' || base === 'home') ? basename(dirname(name)) : base;
1027
+ const title = toTitleCase(titleBase || base);
1028
+ const url = relativePath.startsWith('/') ? relativePath : '/' + relativePath;
1029
+
1030
+ // Remove old entry for this URL, add updated one
1031
+ let activity = devState.recentActivity.filter(r => r.url !== url);
1032
+ activity.push({ title, url, mtime: articleStat.mtimeMs });
1033
+ activity.sort((a, b) => b.mtime - a.mtime);
1034
+ devState.recentActivity = activity.slice(0, 10);
1035
+
1036
+ // Write updated file
1037
+ const publicDir = join(devState.output, 'public');
1038
+ await outputFile(join(publicDir, 'recent-activity.json'), JSON.stringify(devState.recentActivity));
1039
+ console.log(`✅ Recent activity updated for ${name}`);
1040
+ } catch (e) {
1041
+ // ignore
1042
+ }
1043
+ }
1044
+
959
1045
  // Broadcast reload
960
1046
  broadcast('reload', { file: name });
961
1047
  });
@@ -963,7 +1049,12 @@ export async function dev({
963
1049
  // Watch meta directory for template changes
964
1050
  watch(metaDir, { recursive: true }, async (evt, name) => {
965
1051
  console.log(`🎨 Meta changed: ${name}`);
966
- devState.templates = await getTemplates(metaDir);
1052
+ clearMetaBundleCache();
1053
+ const rawMetaTemplates = await getTemplates(metaDir);
1054
+ // Re-copy meta files and re-bundle
1055
+ const pubDir = join(outputDir, 'public');
1056
+ await copyMetaAssets(metaDir, pubDir);
1057
+ devState.templates = await bundleMetaTemplateAssets(rawMetaTemplates, metaDir, pubDir, { minify: true, sourcemap: false });
967
1058
  broadcast('reload', { file: name });
968
1059
  });
969
1060