@kenjura/ursa 0.76.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 +35 -17
- package/meta/default.css +33 -0
- package/meta/templates/default-template/default.css +1268 -0
- package/meta/{default-template.html → templates/default-template/index.html} +15 -0
- package/meta/{menu.js → templates/default-template/menu.js} +1 -1
- package/meta/templates/default-template/sectionify.js +46 -0
- package/meta/{widgets.js → templates/default-template/widgets.js} +126 -0
- package/package.json +1 -1
- package/src/dev.js +73 -28
- package/src/helper/assetBundler.js +471 -0
- package/src/helper/build/autoIndex.js +24 -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/findStyleCss.js +29 -0
- package/src/helper/portUtils.js +132 -0
- package/src/jobs/generate.js +228 -60
- package/src/serve.js +446 -162
- package/meta/character-sheet.css +0 -50
- /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
package/src/jobs/generate.js
CHANGED
|
@@ -26,9 +26,12 @@ import {
|
|
|
26
26
|
import { getAndIncrementBuildId } from "../helper/ursaConfig.js";
|
|
27
27
|
import { extractSections } from "../helper/sectionExtractor.js";
|
|
28
28
|
import { renderFile, renderFileAsync, terminateParserPool } from "../helper/fileRenderer.js";
|
|
29
|
-
import { findStyleCss } from "../helper/findStyleCss.js";
|
|
29
|
+
import { findStyleCss, findAllStyleCss } from "../helper/findStyleCss.js";
|
|
30
30
|
import { findScriptJs, findAllScriptJs } from "../helper/findScriptJs.js";
|
|
31
|
+
import { bundleMetaTemplateAssets, bundleDocumentCss, bundleDocumentJs, clearMetaBundleCache, generateSeparateCssTags, generateSeparateJsTags } from "../helper/assetBundler.js";
|
|
31
32
|
import { buildFullTextIndex, buildIncrementalIndex, loadIndexCache, saveIndexCache } from "../helper/fullTextIndex.js";
|
|
33
|
+
import { dependencyTracker } from "../helper/dependencyTracker.js";
|
|
34
|
+
import { CacheBustHashMap } from "../helper/build/cacheBust.js";
|
|
32
35
|
import { copy as copyDir, emptyDir, outputFile } from "fs-extra";
|
|
33
36
|
import { basename, dirname, extname, join, parse, resolve } from "path";
|
|
34
37
|
import { URL } from "url";
|
|
@@ -71,6 +74,7 @@ import {
|
|
|
71
74
|
getFooter,
|
|
72
75
|
generateAutoIndices,
|
|
73
76
|
generateAutoIndexHtmlFromSource,
|
|
77
|
+
copyMetaAssets,
|
|
74
78
|
} from "../helper/build/index.js";
|
|
75
79
|
import { getProfiler } from "../helper/build/profiler.js";
|
|
76
80
|
|
|
@@ -83,10 +87,15 @@ const cssPathCache = new Map();
|
|
|
83
87
|
// Cache for script path lookups to avoid repeated filesystem walks
|
|
84
88
|
const scriptPathCache = new Map();
|
|
85
89
|
|
|
90
|
+
// Cache for document-level CSS/JS bundle paths to avoid re-bundling identical sets
|
|
91
|
+
const docBundleCache = new Map();
|
|
92
|
+
|
|
86
93
|
// Wrapper for clearWatchCache that passes cssPathCache and scriptPathCache
|
|
87
94
|
export function clearWatchCache() {
|
|
88
95
|
clearWatchCacheBase(cssPathCache);
|
|
89
96
|
scriptPathCache.clear();
|
|
97
|
+
docBundleCache.clear();
|
|
98
|
+
clearMetaBundleCache();
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
const progress = new ProgressReporter();
|
|
@@ -116,8 +125,12 @@ export async function generate({
|
|
|
116
125
|
|
|
117
126
|
// Generate cache-busting timestamp for this build
|
|
118
127
|
const cacheBustTimestamp = generateCacheBustTimestamp();
|
|
128
|
+
const cacheBustHashes = new CacheBustHashMap();
|
|
119
129
|
progress.logTimed(`Cache-bust timestamp: ${cacheBustTimestamp}`);
|
|
120
130
|
|
|
131
|
+
// Initialize dependency tracker for this build
|
|
132
|
+
dependencyTracker.init(source);
|
|
133
|
+
|
|
121
134
|
// Clear output directory when --clean is specified
|
|
122
135
|
if (_clean) {
|
|
123
136
|
progress.startTimer('Clean');
|
|
@@ -283,7 +296,27 @@ export async function generate({
|
|
|
283
296
|
// create public folder
|
|
284
297
|
const pub = join(output, "public");
|
|
285
298
|
await mkdir(pub, { recursive: true });
|
|
286
|
-
|
|
299
|
+
|
|
300
|
+
// Copy meta assets with new template folder structure
|
|
301
|
+
const { copiedFiles, orphanedFiles } = await copyMetaAssets(meta, pub);
|
|
302
|
+
|
|
303
|
+
// Warn about orphaned files in meta that aren't part of any template
|
|
304
|
+
if (orphanedFiles.length > 0) {
|
|
305
|
+
console.warn(`\n⚠️ Warning: Found ${orphanedFiles.length} orphaned file(s) in meta directory:`);
|
|
306
|
+
console.warn(` These files are not in meta/templates/ or meta/shared/ and won't be included:`);
|
|
307
|
+
for (const file of orphanedFiles.slice(0, 10)) {
|
|
308
|
+
console.warn(` - ${file}`);
|
|
309
|
+
}
|
|
310
|
+
if (orphanedFiles.length > 10) {
|
|
311
|
+
console.warn(` ... and ${orphanedFiles.length - 10} more`);
|
|
312
|
+
}
|
|
313
|
+
console.warn(` Move them to meta/templates/{templateName}/ or meta/shared/ to include them.\n`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Bundle meta template assets (CSS + JS) into single files per template
|
|
317
|
+
// This must happen after copying meta to public but before cache-busting
|
|
318
|
+
templates = await bundleMetaTemplateAssets(templates, meta, pub, { minify: true, sourcemap: false });
|
|
319
|
+
progress.logTimed(`Meta template assets bundled`);
|
|
287
320
|
|
|
288
321
|
// Process all CSS files in the entire output directory tree for cache-busting
|
|
289
322
|
const allOutputFiles = await recurse(output, [() => false]);
|
|
@@ -596,65 +629,97 @@ export async function generate({
|
|
|
596
629
|
}
|
|
597
630
|
}
|
|
598
631
|
|
|
599
|
-
// Find
|
|
600
|
-
//
|
|
632
|
+
// Find all style.css files up the tree and bundle them into a single CSS file per folder path
|
|
633
|
+
// (Generate mode: one CSS bundle per unique folder, minimizing requests per page load)
|
|
601
634
|
let styleLink = "";
|
|
602
635
|
try {
|
|
603
|
-
// For root-level files, dir may be "/" which would resolve to filesystem root
|
|
604
|
-
// Use source directory directly in that case
|
|
605
636
|
const dirKey = (dir === "/" || dir === "") ? _source : resolve(_source, dir);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
637
|
+
const folderRelative = (dir === "/" || dir === "") ? "" : dir;
|
|
638
|
+
|
|
639
|
+
// Check bundle cache first (dirs with same CSS ancestry share the same bundle)
|
|
640
|
+
let cachedBundleUrl = docBundleCache.get(`css:${dirKey}`);
|
|
641
|
+
if (cachedBundleUrl !== undefined) {
|
|
642
|
+
if (cachedBundleUrl) {
|
|
643
|
+
styleLink = `<link rel="stylesheet" href="${cachedBundleUrl}" />`;
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
let cssPaths = cssPathCache.get(dirKey);
|
|
647
|
+
if (cssPaths === undefined) {
|
|
648
|
+
cssPaths = await findAllStyleCss(dirKey, _source);
|
|
649
|
+
cssPathCache.set(dirKey, cssPaths);
|
|
650
|
+
}
|
|
651
|
+
if (cssPaths.length > 0) {
|
|
652
|
+
// Copy all source CSS files to output (still needed for serve mode fallback)
|
|
653
|
+
for (const cssPath of cssPaths) {
|
|
654
|
+
if (!copiedCssFiles.has(cssPath)) {
|
|
655
|
+
const cssOutputPath = cssPath.replace(source, output);
|
|
656
|
+
const cssContent = await readFile(cssPath, 'utf8');
|
|
657
|
+
await outputFile(cssOutputPath, cssContent);
|
|
658
|
+
copiedCssFiles.add(cssPath);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Bundle into a single file
|
|
662
|
+
const bundleUrl = await bundleDocumentCss(cssPaths, output, source, folderRelative, { minify: true });
|
|
663
|
+
docBundleCache.set(`css:${dirKey}`, bundleUrl);
|
|
664
|
+
styleLink = `<link rel="stylesheet" href="${bundleUrl}" />`;
|
|
665
|
+
} else {
|
|
666
|
+
docBundleCache.set(`css:${dirKey}`, null);
|
|
621
667
|
}
|
|
622
|
-
|
|
623
|
-
// Generate link tag
|
|
624
|
-
styleLink = `<link rel="stylesheet" href="${cssUrlPath}" />`;
|
|
625
668
|
}
|
|
626
669
|
} catch (e) {
|
|
627
670
|
// ignore
|
|
628
671
|
console.error(e);
|
|
629
672
|
}
|
|
630
673
|
|
|
631
|
-
// Find all script.js
|
|
632
|
-
//
|
|
674
|
+
// Find all script.js files from docroot to current dir and bundle them
|
|
675
|
+
// (Generate mode: one JS bundle per unique folder, external not inlined)
|
|
633
676
|
let customScript = "";
|
|
634
677
|
try {
|
|
635
678
|
const dirKey = (dir === "/" || dir === "") ? _source : resolve(_source, dir);
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
679
|
+
const folderRelative = (dir === "/" || dir === "") ? "" : dir;
|
|
680
|
+
|
|
681
|
+
let cachedBundleUrl = docBundleCache.get(`js:${dirKey}`);
|
|
682
|
+
if (cachedBundleUrl !== undefined) {
|
|
683
|
+
if (cachedBundleUrl) {
|
|
684
|
+
customScript = `<script src="${cachedBundleUrl}"></script>`;
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
let scriptPaths = scriptPathCache.get(dirKey);
|
|
688
|
+
if (scriptPaths === undefined) {
|
|
689
|
+
scriptPaths = await findAllScriptJs(dirKey, _source);
|
|
690
|
+
scriptPathCache.set(dirKey, scriptPaths);
|
|
691
|
+
}
|
|
692
|
+
if (scriptPaths.length > 0) {
|
|
693
|
+
const bundleUrl = await bundleDocumentJs(scriptPaths, output, source, folderRelative, { minify: true });
|
|
694
|
+
docBundleCache.set(`js:${dirKey}`, bundleUrl);
|
|
695
|
+
customScript = `<script src="${bundleUrl}"></script>`;
|
|
696
|
+
} else {
|
|
697
|
+
docBundleCache.set(`js:${dirKey}`, null);
|
|
698
|
+
}
|
|
645
699
|
}
|
|
646
|
-
customScript = scriptTags.join('\n');
|
|
647
700
|
} catch (e) {
|
|
648
701
|
// ignore
|
|
649
702
|
console.error(e);
|
|
650
703
|
}
|
|
651
704
|
|
|
652
705
|
const requestedTemplateName = fileMeta && fileMeta.template;
|
|
653
|
-
const
|
|
654
|
-
|
|
706
|
+
const templateName = requestedTemplateName || DEFAULT_TEMPLATE_NAME;
|
|
707
|
+
const template = templates[templateName];
|
|
655
708
|
|
|
656
709
|
if (!template) {
|
|
657
|
-
throw new Error(`Template not found. Requested: "${
|
|
710
|
+
throw new Error(`Template not found. Requested: "${templateName}". Available templates: ${Object.keys(templates).join(', ') || 'none'}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Register this document's dependencies for invalidation tracking
|
|
714
|
+
{
|
|
715
|
+
const dirKey = (dir === "/" || dir === "") ? _source : resolve(_source, dir);
|
|
716
|
+
const cssDeps = cssPathCache.get(dirKey) || [];
|
|
717
|
+
const jsDeps = scriptPathCache.get(dirKey) || [];
|
|
718
|
+
dependencyTracker.registerDocument(file, {
|
|
719
|
+
templateName,
|
|
720
|
+
cssPaths: cssDeps,
|
|
721
|
+
scriptPaths: jsDeps,
|
|
722
|
+
});
|
|
658
723
|
}
|
|
659
724
|
|
|
660
725
|
// Check if this file has a custom menu
|
|
@@ -1026,11 +1091,15 @@ export async function generate({
|
|
|
1026
1091
|
watchModeCache.meta = meta;
|
|
1027
1092
|
watchModeCache.output = output;
|
|
1028
1093
|
watchModeCache.hashCache = hashCache;
|
|
1094
|
+
watchModeCache.cacheBustTimestamp = cacheBustTimestamp;
|
|
1095
|
+
watchModeCache.cacheBustHashes = cacheBustHashes;
|
|
1096
|
+
watchModeCache.allArticlePaths = [...allSourceFilenamesThatAreArticles];
|
|
1029
1097
|
watchModeCache.imageMap = imageMap;
|
|
1030
1098
|
watchModeCache.customMenus = customMenus;
|
|
1031
1099
|
watchModeCache.lastFullBuild = Date.now();
|
|
1032
1100
|
watchModeCache.isInitialized = true;
|
|
1033
|
-
|
|
1101
|
+
const depStats = dependencyTracker.getStats();
|
|
1102
|
+
progress.log(`Watch cache initialized (${depStats.totalDocuments} documents, ${depStats.uniqueFiles} dependencies tracked)`);
|
|
1034
1103
|
|
|
1035
1104
|
// Write error report if there were any errors
|
|
1036
1105
|
if (errors.length > 0) {
|
|
@@ -1085,6 +1154,104 @@ export async function generate({
|
|
|
1085
1154
|
};
|
|
1086
1155
|
}
|
|
1087
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Regenerate multiple documents affected by a dependency change (e.g., style.css, script.js, template).
|
|
1159
|
+
* Uses the watchModeCache and dependency tracker to efficiently re-render affected documents
|
|
1160
|
+
* with updated cache-bust timestamps.
|
|
1161
|
+
*
|
|
1162
|
+
* @param {string[]} documentPaths - Absolute paths to documents to regenerate
|
|
1163
|
+
* @param {Object} options
|
|
1164
|
+
* @param {string} options._source - Source directory
|
|
1165
|
+
* @param {string} options._meta - Meta directory
|
|
1166
|
+
* @param {string} options._output - Output directory
|
|
1167
|
+
* @param {string} [options.reason] - Reason for regeneration (for logging)
|
|
1168
|
+
* @param {string[]} [options.priorityPaths] - Document paths to regenerate first (e.g. client-viewed docs)
|
|
1169
|
+
* @param {function} [options.onPriorityComplete] - Callback after priority paths are done (receives { regenerated, failed })
|
|
1170
|
+
* @returns {Promise<{success: boolean, message: string, regenerated: number, failed: number}>}
|
|
1171
|
+
*/
|
|
1172
|
+
export async function regenerateAffectedDocuments(documentPaths, {
|
|
1173
|
+
_source,
|
|
1174
|
+
_meta,
|
|
1175
|
+
_output,
|
|
1176
|
+
reason = "dependency change",
|
|
1177
|
+
priorityPaths = [],
|
|
1178
|
+
onPriorityComplete = null,
|
|
1179
|
+
} = {}) {
|
|
1180
|
+
const startTime = Date.now();
|
|
1181
|
+
|
|
1182
|
+
if (!watchModeCache.isInitialized) {
|
|
1183
|
+
return { success: false, message: "Cache not initialized - need full build first", regenerated: 0, failed: 0 };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (documentPaths.length === 0) {
|
|
1187
|
+
return { success: true, message: "No documents to regenerate", regenerated: 0, failed: 0 };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Generate a fresh cache-bust timestamp for this invalidation pass
|
|
1191
|
+
const newTimestamp = generateCacheBustTimestamp();
|
|
1192
|
+
watchModeCache.cacheBustTimestamp = newTimestamp;
|
|
1193
|
+
|
|
1194
|
+
let regenerated = 0;
|
|
1195
|
+
let failed = 0;
|
|
1196
|
+
|
|
1197
|
+
// Separate priority paths from the rest
|
|
1198
|
+
const prioritySet = new Set(priorityPaths.map(p => resolve(p)));
|
|
1199
|
+
const priorityDocs = documentPaths.filter(p => prioritySet.has(resolve(p)));
|
|
1200
|
+
const remainingDocs = documentPaths.filter(p => !prioritySet.has(resolve(p)));
|
|
1201
|
+
|
|
1202
|
+
if (priorityDocs.length > 0) {
|
|
1203
|
+
console.log(`🔄 Regenerating ${priorityDocs.length} priority documents first, then ${remainingDocs.length} remaining (${reason})`);
|
|
1204
|
+
} else {
|
|
1205
|
+
console.log(`🔄 Regenerating ${documentPaths.length} documents (${reason})`);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Process priority documents first
|
|
1209
|
+
for (const docPath of priorityDocs) {
|
|
1210
|
+
try {
|
|
1211
|
+
const result = await regenerateSingleFile(docPath, { _source, _meta, _output });
|
|
1212
|
+
if (result.success) {
|
|
1213
|
+
regenerated++;
|
|
1214
|
+
} else {
|
|
1215
|
+
console.warn(` ⚠️ ${docPath}: ${result.message}`);
|
|
1216
|
+
failed++;
|
|
1217
|
+
}
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
console.error(` ❌ ${docPath}: ${e.message}`);
|
|
1220
|
+
failed++;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Notify caller that priority docs are done (so server can reload those clients immediately)
|
|
1225
|
+
if (priorityDocs.length > 0 && onPriorityComplete) {
|
|
1226
|
+
try {
|
|
1227
|
+
onPriorityComplete({ regenerated, failed, priorityDocs });
|
|
1228
|
+
} catch (e) {
|
|
1229
|
+
console.error(` ⚠️ onPriorityComplete callback error: ${e.message}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Process remaining documents
|
|
1234
|
+
for (const docPath of remainingDocs) {
|
|
1235
|
+
try {
|
|
1236
|
+
const result = await regenerateSingleFile(docPath, { _source, _meta, _output });
|
|
1237
|
+
if (result.success) {
|
|
1238
|
+
regenerated++;
|
|
1239
|
+
} else {
|
|
1240
|
+
console.warn(` ⚠️ ${docPath}: ${result.message}`);
|
|
1241
|
+
failed++;
|
|
1242
|
+
}
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
console.error(` ❌ ${docPath}: ${e.message}`);
|
|
1245
|
+
failed++;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const elapsed = Date.now() - startTime;
|
|
1250
|
+
const msg = `Regenerated ${regenerated}/${documentPaths.length} documents in ${elapsed}ms (${reason})${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
1251
|
+
console.log(`✅ ${msg}`);
|
|
1252
|
+
return { success: failed === 0, message: msg, regenerated, failed };
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1088
1255
|
/**
|
|
1089
1256
|
* Regenerate a single file without scanning the entire source directory.
|
|
1090
1257
|
* This is much faster for watch mode - only regenerate what changed.
|
|
@@ -1204,23 +1371,20 @@ export async function regenerateSingleFile(changedFile, {
|
|
|
1204
1371
|
}
|
|
1205
1372
|
}
|
|
1206
1373
|
|
|
1207
|
-
// Find CSS and
|
|
1374
|
+
// Find all CSS files up the tree and create separate link tags
|
|
1375
|
+
// (regenerateSingleFile is used in serve mode, so separate tags per level for invalidation)
|
|
1208
1376
|
let styleLink = "";
|
|
1209
1377
|
try {
|
|
1210
|
-
// For root-level files, dir may be "/" which would resolve to filesystem root
|
|
1211
1378
|
const dirKey = (dir === "/" || dir === "") ? _source : resolve(_source, dir);
|
|
1212
|
-
const
|
|
1213
|
-
if (
|
|
1214
|
-
//
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
// Generate link tag
|
|
1223
|
-
styleLink = `<link rel="stylesheet" href="${cssUrlPath}" />`;
|
|
1379
|
+
const cssPaths = await findAllStyleCss(dirKey, _source);
|
|
1380
|
+
if (cssPaths.length > 0) {
|
|
1381
|
+
// Copy all CSS files to output (always copy in single-file mode to ensure up to date)
|
|
1382
|
+
for (const cssPath of cssPaths) {
|
|
1383
|
+
const cssOutputPath = cssPath.replace(source, output);
|
|
1384
|
+
const cssContent = await readFile(cssPath, 'utf8');
|
|
1385
|
+
await outputFile(cssOutputPath, cssContent);
|
|
1386
|
+
}
|
|
1387
|
+
styleLink = generateSeparateCssTags(cssPaths, source);
|
|
1224
1388
|
}
|
|
1225
1389
|
} catch (e) {
|
|
1226
1390
|
// ignore
|
|
@@ -1235,17 +1399,21 @@ export async function regenerateSingleFile(changedFile, {
|
|
|
1235
1399
|
return { success: false, message: `Template not found: ${requestedTemplateName || DEFAULT_TEMPLATE_NAME}` };
|
|
1236
1400
|
}
|
|
1237
1401
|
|
|
1238
|
-
// Find all script.js
|
|
1402
|
+
// Find all script.js files from docroot to current dir and serve as separate external tags
|
|
1403
|
+
// (Serve mode: separate tags per level for individual invalidation)
|
|
1239
1404
|
let customScript = "";
|
|
1240
1405
|
try {
|
|
1241
1406
|
const dirKey = (dir === "/" || dir === "") ? _source : resolve(_source, dir);
|
|
1242
1407
|
const scriptPaths = await findAllScriptJs(dirKey, _source);
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1408
|
+
if (scriptPaths.length > 0) {
|
|
1409
|
+
// Copy all script files to output so they can be served
|
|
1410
|
+
for (const scriptPath of scriptPaths) {
|
|
1411
|
+
const scriptOutputPath = scriptPath.replace(source, output);
|
|
1412
|
+
const scriptContent = await readFile(scriptPath, 'utf8');
|
|
1413
|
+
await outputFile(scriptOutputPath, scriptContent);
|
|
1414
|
+
}
|
|
1415
|
+
customScript = generateSeparateJsTags(scriptPaths, source);
|
|
1247
1416
|
}
|
|
1248
|
-
customScript = scriptTags.join('\n');
|
|
1249
1417
|
} catch (e) {
|
|
1250
1418
|
// ignore
|
|
1251
1419
|
}
|