bunki 0.17.0 → 0.17.1
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/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli.js +309 -59
- package/dist/index.js +287 -43
- package/dist/parser.d.ts +8 -0
- package/dist/site-generator.d.ts +10 -0
- package/dist/utils/build-cache.d.ts +76 -0
- package/dist/utils/change-detector.d.ts +40 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ export declare function handleGenerateCommand(options: {
|
|
|
12
12
|
content: string;
|
|
13
13
|
output: string;
|
|
14
14
|
templates: string;
|
|
15
|
+
incremental?: boolean;
|
|
15
16
|
}, deps?: GenerateDeps): Promise<void>;
|
|
16
17
|
export declare function registerGenerateCommand(program: Command): Command;
|
|
17
18
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -28478,12 +28478,12 @@ function registerCssCommand(program2) {
|
|
|
28478
28478
|
}
|
|
28479
28479
|
|
|
28480
28480
|
// src/cli/commands/generate.ts
|
|
28481
|
-
import
|
|
28481
|
+
import path10 from "path";
|
|
28482
28482
|
|
|
28483
28483
|
// src/site-generator.ts
|
|
28484
28484
|
var import_nunjucks2 = __toESM(require_nunjucks(), 1);
|
|
28485
28485
|
var import_slugify = __toESM(require_slugify(), 1);
|
|
28486
|
-
import
|
|
28486
|
+
import path9 from "path";
|
|
28487
28487
|
|
|
28488
28488
|
// src/parser.ts
|
|
28489
28489
|
import path5 from "path";
|
|
@@ -33368,6 +33368,20 @@ function detectFileConflicts(files) {
|
|
|
33368
33368
|
}
|
|
33369
33369
|
return errors;
|
|
33370
33370
|
}
|
|
33371
|
+
async function parseMarkdownFiles(filePaths, cdnConfig) {
|
|
33372
|
+
const resultsPromises = filePaths.map((filePath) => parseMarkdownFile(filePath, cdnConfig).then((result) => ({
|
|
33373
|
+
result,
|
|
33374
|
+
filePath
|
|
33375
|
+
})));
|
|
33376
|
+
const results = await Promise.all(resultsPromises);
|
|
33377
|
+
const postsWithPaths = [];
|
|
33378
|
+
for (const { result, filePath } of results) {
|
|
33379
|
+
if (result.post) {
|
|
33380
|
+
postsWithPaths.push({ post: result.post, filePath });
|
|
33381
|
+
}
|
|
33382
|
+
}
|
|
33383
|
+
return postsWithPaths;
|
|
33384
|
+
}
|
|
33371
33385
|
async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
|
|
33372
33386
|
try {
|
|
33373
33387
|
const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
|
|
@@ -33638,6 +33652,165 @@ function generateHomePageSchemas(options2) {
|
|
|
33638
33652
|
return schemas;
|
|
33639
33653
|
}
|
|
33640
33654
|
|
|
33655
|
+
// src/utils/build-cache.ts
|
|
33656
|
+
var {hash: hash2 } = globalThis.Bun;
|
|
33657
|
+
import path6 from "path";
|
|
33658
|
+
var CACHE_VERSION = "2.0.0";
|
|
33659
|
+
var CACHE_FILENAME = ".bunki-cache.json";
|
|
33660
|
+
async function hashFile(filePath) {
|
|
33661
|
+
try {
|
|
33662
|
+
const file = Bun.file(filePath);
|
|
33663
|
+
const content = await file.arrayBuffer();
|
|
33664
|
+
return hash2(content).toString(36);
|
|
33665
|
+
} catch (error) {
|
|
33666
|
+
return "";
|
|
33667
|
+
}
|
|
33668
|
+
}
|
|
33669
|
+
async function getFileMtime(filePath) {
|
|
33670
|
+
try {
|
|
33671
|
+
const stat = await Bun.file(filePath).stat();
|
|
33672
|
+
return stat?.mtime?.getTime() || 0;
|
|
33673
|
+
} catch (error) {
|
|
33674
|
+
return 0;
|
|
33675
|
+
}
|
|
33676
|
+
}
|
|
33677
|
+
async function loadCache(cwd) {
|
|
33678
|
+
const cachePath = path6.join(cwd, CACHE_FILENAME);
|
|
33679
|
+
const cacheFile = Bun.file(cachePath);
|
|
33680
|
+
try {
|
|
33681
|
+
if (await cacheFile.exists()) {
|
|
33682
|
+
const content = await cacheFile.text();
|
|
33683
|
+
const cache = JSON.parse(content);
|
|
33684
|
+
if (cache.version !== CACHE_VERSION) {
|
|
33685
|
+
console.log(`Cache version mismatch (${cache.version} vs ${CACHE_VERSION}), rebuilding...`);
|
|
33686
|
+
return createEmptyCache();
|
|
33687
|
+
}
|
|
33688
|
+
return cache;
|
|
33689
|
+
}
|
|
33690
|
+
} catch (error) {
|
|
33691
|
+
console.warn("Error loading cache, rebuilding:", error);
|
|
33692
|
+
}
|
|
33693
|
+
return createEmptyCache();
|
|
33694
|
+
}
|
|
33695
|
+
async function saveCache(cwd, cache) {
|
|
33696
|
+
const cachePath = path6.join(cwd, CACHE_FILENAME);
|
|
33697
|
+
try {
|
|
33698
|
+
await Bun.write(cachePath, JSON.stringify(cache, null, 2));
|
|
33699
|
+
} catch (error) {
|
|
33700
|
+
console.warn("Error saving cache:", error);
|
|
33701
|
+
}
|
|
33702
|
+
}
|
|
33703
|
+
function createEmptyCache() {
|
|
33704
|
+
return {
|
|
33705
|
+
version: CACHE_VERSION,
|
|
33706
|
+
files: {}
|
|
33707
|
+
};
|
|
33708
|
+
}
|
|
33709
|
+
async function hasFileChanged(filePath, cache) {
|
|
33710
|
+
const cached = cache.files[filePath];
|
|
33711
|
+
if (!cached) {
|
|
33712
|
+
return true;
|
|
33713
|
+
}
|
|
33714
|
+
const currentMtime = await getFileMtime(filePath);
|
|
33715
|
+
if (currentMtime !== cached.mtime) {
|
|
33716
|
+
const currentHash = await hashFile(filePath);
|
|
33717
|
+
return currentHash !== cached.hash;
|
|
33718
|
+
}
|
|
33719
|
+
return false;
|
|
33720
|
+
}
|
|
33721
|
+
async function updateCacheEntry(filePath, cache, options2) {
|
|
33722
|
+
const currentHash = await hashFile(filePath);
|
|
33723
|
+
const currentMtime = await getFileMtime(filePath);
|
|
33724
|
+
cache.files[filePath] = {
|
|
33725
|
+
hash: currentHash,
|
|
33726
|
+
mtime: currentMtime,
|
|
33727
|
+
post: options2?.post,
|
|
33728
|
+
outputs: options2?.outputs
|
|
33729
|
+
};
|
|
33730
|
+
}
|
|
33731
|
+
async function hasConfigChanged(configPath, cache) {
|
|
33732
|
+
const currentHash = await hashFile(configPath);
|
|
33733
|
+
if (!cache.configHash) {
|
|
33734
|
+
cache.configHash = currentHash;
|
|
33735
|
+
return true;
|
|
33736
|
+
}
|
|
33737
|
+
if (currentHash !== cache.configHash) {
|
|
33738
|
+
cache.configHash = currentHash;
|
|
33739
|
+
return true;
|
|
33740
|
+
}
|
|
33741
|
+
return false;
|
|
33742
|
+
}
|
|
33743
|
+
function loadCachedPosts(cache, filePaths) {
|
|
33744
|
+
const posts = [];
|
|
33745
|
+
for (const filePath of filePaths) {
|
|
33746
|
+
const entry = cache.files[filePath];
|
|
33747
|
+
if (entry?.post) {
|
|
33748
|
+
posts.push(entry.post);
|
|
33749
|
+
}
|
|
33750
|
+
}
|
|
33751
|
+
return posts;
|
|
33752
|
+
}
|
|
33753
|
+
|
|
33754
|
+
// src/utils/change-detector.ts
|
|
33755
|
+
async function detectChanges(currentFiles, cache, options2 = {}) {
|
|
33756
|
+
const changes = {
|
|
33757
|
+
changedPosts: [],
|
|
33758
|
+
deletedPosts: [],
|
|
33759
|
+
stylesChanged: false,
|
|
33760
|
+
configChanged: false,
|
|
33761
|
+
templatesChanged: false,
|
|
33762
|
+
fullRebuild: false
|
|
33763
|
+
};
|
|
33764
|
+
if (options2.configPath) {
|
|
33765
|
+
const configChanged = await hasFileChanged(options2.configPath, cache);
|
|
33766
|
+
if (configChanged) {
|
|
33767
|
+
changes.configChanged = true;
|
|
33768
|
+
changes.fullRebuild = true;
|
|
33769
|
+
return changes;
|
|
33770
|
+
}
|
|
33771
|
+
}
|
|
33772
|
+
if (options2.templatePaths && options2.templatePaths.length > 0) {
|
|
33773
|
+
for (const templatePath of options2.templatePaths) {
|
|
33774
|
+
const changed = await hasFileChanged(templatePath, cache);
|
|
33775
|
+
if (changed) {
|
|
33776
|
+
changes.templatesChanged = true;
|
|
33777
|
+
changes.fullRebuild = true;
|
|
33778
|
+
return changes;
|
|
33779
|
+
}
|
|
33780
|
+
}
|
|
33781
|
+
}
|
|
33782
|
+
if (options2.stylesPaths && options2.stylesPaths.length > 0) {
|
|
33783
|
+
for (const stylePath of options2.stylesPaths) {
|
|
33784
|
+
const changed = await hasFileChanged(stylePath, cache);
|
|
33785
|
+
if (changed) {
|
|
33786
|
+
changes.stylesChanged = true;
|
|
33787
|
+
break;
|
|
33788
|
+
}
|
|
33789
|
+
}
|
|
33790
|
+
}
|
|
33791
|
+
for (const filePath of currentFiles) {
|
|
33792
|
+
const changed = await hasFileChanged(filePath, cache);
|
|
33793
|
+
if (changed) {
|
|
33794
|
+
changes.changedPosts.push(filePath);
|
|
33795
|
+
}
|
|
33796
|
+
}
|
|
33797
|
+
const cachedFiles = Object.keys(cache.files).filter((f) => f.endsWith(".md"));
|
|
33798
|
+
for (const cachedFile of cachedFiles) {
|
|
33799
|
+
if (!currentFiles.includes(cachedFile)) {
|
|
33800
|
+
changes.deletedPosts.push(cachedFile);
|
|
33801
|
+
}
|
|
33802
|
+
}
|
|
33803
|
+
if (changes.deletedPosts.length > 0) {
|
|
33804
|
+
changes.fullRebuild = true;
|
|
33805
|
+
}
|
|
33806
|
+
return changes;
|
|
33807
|
+
}
|
|
33808
|
+
function estimateTimeSaved(totalPosts, changedPosts) {
|
|
33809
|
+
const avgTimePerPost = 6;
|
|
33810
|
+
const skippedPosts = totalPosts - changedPosts;
|
|
33811
|
+
return skippedPosts * avgTimePerPost;
|
|
33812
|
+
}
|
|
33813
|
+
|
|
33641
33814
|
// src/utils/xml-builder.ts
|
|
33642
33815
|
function escapeXml(text) {
|
|
33643
33816
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -33870,14 +34043,14 @@ Sitemap: ${config.baseUrl}/sitemap.xml
|
|
|
33870
34043
|
|
|
33871
34044
|
// src/generators/pages.ts
|
|
33872
34045
|
var import_nunjucks = __toESM(require_nunjucks(), 1);
|
|
33873
|
-
import
|
|
34046
|
+
import path7 from "path";
|
|
33874
34047
|
function getSortedTags(tags, limit) {
|
|
33875
34048
|
const sorted = Object.values(tags).sort((a, b2) => b2.count - a.count);
|
|
33876
34049
|
return limit ? sorted.slice(0, limit) : sorted;
|
|
33877
34050
|
}
|
|
33878
34051
|
async function writeHtmlFile(outputDir, relativePath, content) {
|
|
33879
|
-
const fullPath =
|
|
33880
|
-
const dir =
|
|
34052
|
+
const fullPath = path7.join(outputDir, relativePath);
|
|
34053
|
+
const dir = path7.dirname(fullPath);
|
|
33881
34054
|
await ensureDir(dir);
|
|
33882
34055
|
await Bun.write(fullPath, content);
|
|
33883
34056
|
}
|
|
@@ -34053,7 +34226,7 @@ async function generateMapPage(site, config, outputDir) {
|
|
|
34053
34226
|
|
|
34054
34227
|
// src/generators/assets.ts
|
|
34055
34228
|
var {Glob: Glob2 } = globalThis.Bun;
|
|
34056
|
-
import
|
|
34229
|
+
import path8 from "path";
|
|
34057
34230
|
async function generateStylesheet(config, outputDir) {
|
|
34058
34231
|
const cssConfig = config.css || getDefaultCSSConfig();
|
|
34059
34232
|
if (!cssConfig.enabled) {
|
|
@@ -34074,15 +34247,15 @@ async function generateStylesheet(config, outputDir) {
|
|
|
34074
34247
|
}
|
|
34075
34248
|
}
|
|
34076
34249
|
async function fallbackCSSGeneration(cssConfig, outputDir) {
|
|
34077
|
-
const cssFilePath =
|
|
34250
|
+
const cssFilePath = path8.resolve(process.cwd(), cssConfig.input);
|
|
34078
34251
|
const cssFile = Bun.file(cssFilePath);
|
|
34079
34252
|
if (!await cssFile.exists()) {
|
|
34080
34253
|
console.warn(`CSS input file not found: ${cssFilePath}`);
|
|
34081
34254
|
return;
|
|
34082
34255
|
}
|
|
34083
34256
|
try {
|
|
34084
|
-
const outputPath =
|
|
34085
|
-
const outputDirPath =
|
|
34257
|
+
const outputPath = path8.resolve(outputDir, cssConfig.output);
|
|
34258
|
+
const outputDirPath = path8.dirname(outputPath);
|
|
34086
34259
|
await ensureDir(outputDirPath);
|
|
34087
34260
|
await Bun.write(outputPath, cssFile);
|
|
34088
34261
|
console.log("\u2705 CSS file copied successfully (fallback mode)");
|
|
@@ -34091,19 +34264,19 @@ async function fallbackCSSGeneration(cssConfig, outputDir) {
|
|
|
34091
34264
|
}
|
|
34092
34265
|
}
|
|
34093
34266
|
async function copyStaticAssets(templatesDir, outputDir) {
|
|
34094
|
-
const assetsDir =
|
|
34095
|
-
const publicDir =
|
|
34267
|
+
const assetsDir = path8.join(templatesDir, "assets");
|
|
34268
|
+
const publicDir = path8.join(process.cwd(), "public");
|
|
34096
34269
|
if (await isDirectory(assetsDir)) {
|
|
34097
34270
|
const assetGlob = new Glob2("**/*.*");
|
|
34098
|
-
const assetsOutputDir =
|
|
34271
|
+
const assetsOutputDir = path8.join(outputDir, "assets");
|
|
34099
34272
|
await ensureDir(assetsOutputDir);
|
|
34100
34273
|
for await (const file of assetGlob.scan({
|
|
34101
34274
|
cwd: assetsDir,
|
|
34102
34275
|
absolute: true
|
|
34103
34276
|
})) {
|
|
34104
|
-
const relativePath =
|
|
34105
|
-
const targetPath =
|
|
34106
|
-
const targetDir =
|
|
34277
|
+
const relativePath = path8.relative(assetsDir, file);
|
|
34278
|
+
const targetPath = path8.join(assetsOutputDir, relativePath);
|
|
34279
|
+
const targetDir = path8.dirname(targetPath);
|
|
34107
34280
|
await ensureDir(targetDir);
|
|
34108
34281
|
await copyFile(file, targetPath);
|
|
34109
34282
|
}
|
|
@@ -34117,9 +34290,9 @@ async function copyStaticAssets(templatesDir, outputDir) {
|
|
|
34117
34290
|
})) {
|
|
34118
34291
|
if (await isDirectory(file))
|
|
34119
34292
|
continue;
|
|
34120
|
-
const relativePath =
|
|
34121
|
-
const destPath =
|
|
34122
|
-
const targetDir =
|
|
34293
|
+
const relativePath = path8.relative(publicDir, file);
|
|
34294
|
+
const destPath = path8.join(outputDir, relativePath);
|
|
34295
|
+
const targetDir = path8.dirname(destPath);
|
|
34123
34296
|
await ensureDir(targetDir);
|
|
34124
34297
|
await copyFile(file, destPath);
|
|
34125
34298
|
}
|
|
@@ -34201,6 +34374,8 @@ class SiteGenerator {
|
|
|
34201
34374
|
options;
|
|
34202
34375
|
site;
|
|
34203
34376
|
metrics;
|
|
34377
|
+
cache = null;
|
|
34378
|
+
incrementalMode = false;
|
|
34204
34379
|
constructor(options2) {
|
|
34205
34380
|
this.options = options2;
|
|
34206
34381
|
this.site = {
|
|
@@ -34246,6 +34421,9 @@ class SiteGenerator {
|
|
|
34246
34421
|
}
|
|
34247
34422
|
});
|
|
34248
34423
|
}
|
|
34424
|
+
enableIncrementalMode() {
|
|
34425
|
+
this.incrementalMode = true;
|
|
34426
|
+
}
|
|
34249
34427
|
async initialize() {
|
|
34250
34428
|
this.metrics.startStage("initialization");
|
|
34251
34429
|
console.log("Initializing site generator...");
|
|
@@ -34254,7 +34432,7 @@ class SiteGenerator {
|
|
|
34254
34432
|
setNoFollowExceptions(this.options.config.noFollowExceptions);
|
|
34255
34433
|
}
|
|
34256
34434
|
let tagDescriptions = {};
|
|
34257
|
-
const tagsTomlPath =
|
|
34435
|
+
const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
|
|
34258
34436
|
const tagsTomlFile = Bun.file(tagsTomlPath);
|
|
34259
34437
|
if (await tagsTomlFile.exists()) {
|
|
34260
34438
|
try {
|
|
@@ -34264,8 +34442,10 @@ class SiteGenerator {
|
|
|
34264
34442
|
console.warn("Error loading tag descriptions:", error);
|
|
34265
34443
|
}
|
|
34266
34444
|
}
|
|
34267
|
-
|
|
34268
|
-
|
|
34445
|
+
if (this.incrementalMode) {
|
|
34446
|
+
this.cache = await loadCache(process.cwd());
|
|
34447
|
+
}
|
|
34448
|
+
const posts = await this.parseContent();
|
|
34269
34449
|
const tags = {};
|
|
34270
34450
|
posts.forEach((post) => {
|
|
34271
34451
|
post.tagSlugs = {};
|
|
@@ -34303,7 +34483,21 @@ class SiteGenerator {
|
|
|
34303
34483
|
console.log("Generating static site...");
|
|
34304
34484
|
await ensureDir(this.options.outputDir);
|
|
34305
34485
|
this.metrics.startStage("cssProcessing");
|
|
34306
|
-
|
|
34486
|
+
let cssChanged = true;
|
|
34487
|
+
if (this.cache && this.incrementalMode && this.options.config.css) {
|
|
34488
|
+
const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
|
|
34489
|
+
const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
|
|
34490
|
+
const cssOutputExists = await Bun.file(cssOutputPath).exists();
|
|
34491
|
+
cssChanged = await hasFileChanged(cssInputPath, this.cache);
|
|
34492
|
+
if (!cssChanged && cssOutputExists) {
|
|
34493
|
+
console.log("\u23ED\uFE0F Skipping CSS (unchanged)");
|
|
34494
|
+
} else {
|
|
34495
|
+
await generateStylesheet(this.options.config, this.options.outputDir);
|
|
34496
|
+
await updateCacheEntry(cssInputPath, this.cache);
|
|
34497
|
+
}
|
|
34498
|
+
} else {
|
|
34499
|
+
await generateStylesheet(this.options.config, this.options.outputDir);
|
|
34500
|
+
}
|
|
34307
34501
|
this.metrics.startStage("pageGeneration");
|
|
34308
34502
|
await Promise.all([
|
|
34309
34503
|
generateIndexPages(this.site, this.options.config, this.options.outputDir),
|
|
@@ -34320,25 +34514,75 @@ class SiteGenerator {
|
|
|
34320
34514
|
const outputStats = await this.calculateOutputStats();
|
|
34321
34515
|
const buildMetrics = this.metrics.getMetrics(outputStats);
|
|
34322
34516
|
displayMetrics(buildMetrics);
|
|
34517
|
+
if (this.cache) {
|
|
34518
|
+
await saveCache(process.cwd(), this.cache);
|
|
34519
|
+
}
|
|
34323
34520
|
}
|
|
34324
34521
|
async generateFeeds() {
|
|
34325
34522
|
const pageSize = 10;
|
|
34326
34523
|
const rssContent = generateRSSFeed(this.site, this.options.config);
|
|
34327
|
-
await Bun.write(
|
|
34524
|
+
await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
|
|
34328
34525
|
const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
|
|
34329
|
-
await Bun.write(
|
|
34526
|
+
await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
|
|
34330
34527
|
console.log("Generated sitemap.xml");
|
|
34331
34528
|
const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
|
|
34332
34529
|
const sitemapSize = sitemapContent.length;
|
|
34333
34530
|
if (urlCount > 1000 || sitemapSize > 40000) {
|
|
34334
34531
|
const sitemapIndexContent = generateSitemapIndex(this.options.config);
|
|
34335
|
-
await Bun.write(
|
|
34532
|
+
await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
|
|
34336
34533
|
console.log("Generated sitemap_index.xml");
|
|
34337
34534
|
}
|
|
34338
34535
|
const robotsTxtContent = generateRobotsTxt(this.options.config);
|
|
34339
|
-
await Bun.write(
|
|
34536
|
+
await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
|
|
34340
34537
|
console.log("Generated robots.txt");
|
|
34341
34538
|
}
|
|
34539
|
+
async parseContent() {
|
|
34540
|
+
const strictMode = this.options.config.strictMode ?? false;
|
|
34541
|
+
if (!this.incrementalMode || !this.cache) {
|
|
34542
|
+
const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
|
|
34543
|
+
if (this.cache) {
|
|
34544
|
+
const allFiles2 = await findFilesByPattern("**/*.md", this.options.contentDir, true);
|
|
34545
|
+
const postsByPath = new Map(posts.map((p) => [p.url, p]));
|
|
34546
|
+
for (let i = 0;i < allFiles2.length; i++) {
|
|
34547
|
+
const filePath = allFiles2[i];
|
|
34548
|
+
const post = posts[i];
|
|
34549
|
+
await updateCacheEntry(filePath, this.cache, { post });
|
|
34550
|
+
}
|
|
34551
|
+
}
|
|
34552
|
+
return posts;
|
|
34553
|
+
}
|
|
34554
|
+
const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
|
|
34555
|
+
const configPath = path9.join(process.cwd(), "bunki.config.ts");
|
|
34556
|
+
const configChanged = await hasConfigChanged(configPath, this.cache);
|
|
34557
|
+
if (configChanged) {
|
|
34558
|
+
console.log("Config changed, full rebuild required");
|
|
34559
|
+
return this.parseContent();
|
|
34560
|
+
}
|
|
34561
|
+
const changes = await detectChanges(allFiles, this.cache);
|
|
34562
|
+
if (changes.fullRebuild) {
|
|
34563
|
+
console.log("Full rebuild required");
|
|
34564
|
+
this.incrementalMode = false;
|
|
34565
|
+
return this.parseContent();
|
|
34566
|
+
}
|
|
34567
|
+
if (changes.changedPosts.length === 0) {
|
|
34568
|
+
console.log("No content changes detected, using cached posts");
|
|
34569
|
+
const cachedPosts2 = loadCachedPosts(this.cache, allFiles);
|
|
34570
|
+
console.log(`\u2728 Loaded ${cachedPosts2.length} posts from cache (0ms parsing)`);
|
|
34571
|
+
return cachedPosts2;
|
|
34572
|
+
}
|
|
34573
|
+
const timeSaved = estimateTimeSaved(allFiles.length, changes.changedPosts.length);
|
|
34574
|
+
console.log(`\uD83D\uDCE6 Incremental build: ${changes.changedPosts.length}/${allFiles.length} files changed (~${timeSaved}ms saved)`);
|
|
34575
|
+
const changedPostsWithPaths = await parseMarkdownFiles(changes.changedPosts, this.options.config.cdn);
|
|
34576
|
+
const unchangedFiles = allFiles.filter((f) => !changes.changedPosts.includes(f));
|
|
34577
|
+
const cachedPosts = loadCachedPosts(this.cache, unchangedFiles);
|
|
34578
|
+
console.log(` Parsed: ${changedPostsWithPaths.length} new/changed, loaded: ${cachedPosts.length} from cache`);
|
|
34579
|
+
const changedPosts = changedPostsWithPaths.map((p) => p.post);
|
|
34580
|
+
const allPosts = [...changedPosts, ...cachedPosts].sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
|
|
34581
|
+
for (const { post, filePath } of changedPostsWithPaths) {
|
|
34582
|
+
await updateCacheEntry(filePath, this.cache, { post });
|
|
34583
|
+
}
|
|
34584
|
+
return allPosts;
|
|
34585
|
+
}
|
|
34342
34586
|
groupPostsByYear(posts) {
|
|
34343
34587
|
const postsByYear = {};
|
|
34344
34588
|
for (const post of posts) {
|
|
@@ -34387,15 +34631,18 @@ var defaultDeps2 = {
|
|
|
34387
34631
|
};
|
|
34388
34632
|
async function handleGenerateCommand(options2, deps = defaultDeps2) {
|
|
34389
34633
|
try {
|
|
34390
|
-
const configPath =
|
|
34391
|
-
const contentDir =
|
|
34392
|
-
const outputDir =
|
|
34393
|
-
const templatesDir =
|
|
34634
|
+
const configPath = path10.resolve(options2.config);
|
|
34635
|
+
const contentDir = path10.resolve(options2.content);
|
|
34636
|
+
const outputDir = path10.resolve(options2.output);
|
|
34637
|
+
const templatesDir = path10.resolve(options2.templates);
|
|
34394
34638
|
deps.logger.log("Generating site with:");
|
|
34395
34639
|
deps.logger.log(`- Config file: ${configPath}`);
|
|
34396
34640
|
deps.logger.log(`- Content directory: ${contentDir}`);
|
|
34397
34641
|
deps.logger.log(`- Output directory: ${outputDir}`);
|
|
34398
34642
|
deps.logger.log(`- Templates directory: ${templatesDir}`);
|
|
34643
|
+
if (options2.incremental) {
|
|
34644
|
+
deps.logger.log(`- Incremental mode: enabled`);
|
|
34645
|
+
}
|
|
34399
34646
|
const config = await deps.loadConfig(configPath);
|
|
34400
34647
|
const generator = deps.createGenerator({
|
|
34401
34648
|
contentDir,
|
|
@@ -34403,6 +34650,9 @@ async function handleGenerateCommand(options2, deps = defaultDeps2) {
|
|
|
34403
34650
|
templatesDir,
|
|
34404
34651
|
config
|
|
34405
34652
|
});
|
|
34653
|
+
if (options2.incremental) {
|
|
34654
|
+
generator.enableIncrementalMode();
|
|
34655
|
+
}
|
|
34406
34656
|
await generator.initialize();
|
|
34407
34657
|
await generator.generate();
|
|
34408
34658
|
deps.logger.log("Site generation completed successfully!");
|
|
@@ -34412,17 +34662,17 @@ async function handleGenerateCommand(options2, deps = defaultDeps2) {
|
|
|
34412
34662
|
}
|
|
34413
34663
|
}
|
|
34414
34664
|
function registerGenerateCommand(program2) {
|
|
34415
|
-
return program2.command("generate").description("Generate static site from markdown content").option("-c, --config <file>", "Config file path", "bunki.config.ts").option("-d, --content <dir>", "Content directory", DEFAULT_CONTENT_DIR).option("-o, --output <dir>", "Output directory", DEFAULT_OUTPUT_DIR).option("-t, --templates <dir>", "Templates directory", DEFAULT_TEMPLATES_DIR).action(async (options2) => {
|
|
34665
|
+
return program2.command("generate").description("Generate static site from markdown content").option("-c, --config <file>", "Config file path", "bunki.config.ts").option("-d, --content <dir>", "Content directory", DEFAULT_CONTENT_DIR).option("-o, --output <dir>", "Output directory", DEFAULT_OUTPUT_DIR).option("-t, --templates <dir>", "Templates directory", DEFAULT_TEMPLATES_DIR).option("-i, --incremental", "Enable incremental builds (only rebuild changed files)").action(async (options2) => {
|
|
34416
34666
|
await handleGenerateCommand(options2);
|
|
34417
34667
|
});
|
|
34418
34668
|
}
|
|
34419
34669
|
|
|
34420
34670
|
// src/utils/image-uploader.ts
|
|
34421
|
-
import
|
|
34671
|
+
import path12 from "path";
|
|
34422
34672
|
|
|
34423
34673
|
// src/utils/s3-uploader.ts
|
|
34424
34674
|
var {S3Client } = globalThis.Bun;
|
|
34425
|
-
import
|
|
34675
|
+
import path11 from "path";
|
|
34426
34676
|
|
|
34427
34677
|
class S3Uploader {
|
|
34428
34678
|
s3Config;
|
|
@@ -34545,8 +34795,8 @@ class S3Uploader {
|
|
|
34545
34795
|
let failedCount = 0;
|
|
34546
34796
|
const uploadTasks = imageFiles.map((imageFile) => async () => {
|
|
34547
34797
|
try {
|
|
34548
|
-
const imagePath =
|
|
34549
|
-
const filename =
|
|
34798
|
+
const imagePath = path11.join(imagesDir, imageFile);
|
|
34799
|
+
const filename = path11.basename(imagePath);
|
|
34550
34800
|
const file = Bun.file(imagePath);
|
|
34551
34801
|
const contentType = file.type;
|
|
34552
34802
|
if (process.env.BUNKI_DRY_RUN === "true") {} else {
|
|
@@ -34580,10 +34830,10 @@ function createUploader(config) {
|
|
|
34580
34830
|
}
|
|
34581
34831
|
|
|
34582
34832
|
// src/utils/image-uploader.ts
|
|
34583
|
-
var DEFAULT_IMAGES_DIR =
|
|
34833
|
+
var DEFAULT_IMAGES_DIR = path12.join(process.cwd(), "assets");
|
|
34584
34834
|
async function uploadImages(options2 = {}) {
|
|
34585
34835
|
try {
|
|
34586
|
-
const imagesDir =
|
|
34836
|
+
const imagesDir = path12.resolve(options2.images || DEFAULT_IMAGES_DIR);
|
|
34587
34837
|
if (!await fileExists(imagesDir)) {
|
|
34588
34838
|
console.log(`Creating images directory at ${imagesDir}...`);
|
|
34589
34839
|
await ensureDir(imagesDir);
|
|
@@ -34620,7 +34870,7 @@ async function uploadImages(options2 = {}) {
|
|
|
34620
34870
|
const uploader = createUploader(s3Config);
|
|
34621
34871
|
const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
|
|
34622
34872
|
if (options2.outputJson) {
|
|
34623
|
-
const outputFile =
|
|
34873
|
+
const outputFile = path12.resolve(options2.outputJson);
|
|
34624
34874
|
await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
|
|
34625
34875
|
console.log(`Image URL mapping saved to ${outputFile}`);
|
|
34626
34876
|
}
|
|
@@ -34666,7 +34916,7 @@ function registerImagesPushCommand(program2) {
|
|
|
34666
34916
|
}
|
|
34667
34917
|
|
|
34668
34918
|
// src/cli/commands/init.ts
|
|
34669
|
-
import
|
|
34919
|
+
import path13 from "path";
|
|
34670
34920
|
|
|
34671
34921
|
// src/cli/commands/templates/base-njk.ts
|
|
34672
34922
|
var baseNjk = String.raw`<!DOCTYPE html>
|
|
@@ -35169,7 +35419,7 @@ var defaultDependencies = {
|
|
|
35169
35419
|
};
|
|
35170
35420
|
async function handleInitCommand(options2, deps = defaultDependencies) {
|
|
35171
35421
|
try {
|
|
35172
|
-
const configPath =
|
|
35422
|
+
const configPath = path13.resolve(options2.config);
|
|
35173
35423
|
const configCreated = await deps.createDefaultConfig(configPath);
|
|
35174
35424
|
if (!configCreated) {
|
|
35175
35425
|
deps.logger.log(`
|
|
@@ -35178,19 +35428,19 @@ Skipped initialization because the config file already exists`);
|
|
|
35178
35428
|
}
|
|
35179
35429
|
deps.logger.log("Creating directory structure...");
|
|
35180
35430
|
const baseDir = process.cwd();
|
|
35181
|
-
const contentDir =
|
|
35182
|
-
const templatesDir =
|
|
35183
|
-
const stylesDir =
|
|
35184
|
-
const publicDir =
|
|
35431
|
+
const contentDir = path13.join(baseDir, "content");
|
|
35432
|
+
const templatesDir = path13.join(baseDir, "templates");
|
|
35433
|
+
const stylesDir = path13.join(templatesDir, "styles");
|
|
35434
|
+
const publicDir = path13.join(baseDir, "public");
|
|
35185
35435
|
await deps.ensureDir(contentDir);
|
|
35186
35436
|
await deps.ensureDir(templatesDir);
|
|
35187
35437
|
await deps.ensureDir(stylesDir);
|
|
35188
35438
|
await deps.ensureDir(publicDir);
|
|
35189
35439
|
for (const [filename, content] of Object.entries(nunjucks3)) {
|
|
35190
|
-
await deps.writeFile(
|
|
35440
|
+
await deps.writeFile(path13.join(templatesDir, filename), content);
|
|
35191
35441
|
}
|
|
35192
|
-
await deps.writeFile(
|
|
35193
|
-
await deps.writeFile(
|
|
35442
|
+
await deps.writeFile(path13.join(stylesDir, "main.css"), defaultCss);
|
|
35443
|
+
await deps.writeFile(path13.join(contentDir, "welcome.md"), samplePost);
|
|
35194
35444
|
deps.logger.log(`
|
|
35195
35445
|
Initialization complete! Here are the next steps:`);
|
|
35196
35446
|
deps.logger.log("1. Edit bunki.config.ts to configure your site");
|
|
@@ -35209,7 +35459,7 @@ function registerInitCommand(program2, deps = defaultDependencies) {
|
|
|
35209
35459
|
}
|
|
35210
35460
|
|
|
35211
35461
|
// src/cli/commands/new-post.ts
|
|
35212
|
-
import
|
|
35462
|
+
import path14 from "path";
|
|
35213
35463
|
var defaultDeps4 = {
|
|
35214
35464
|
writeFile: (filePath, data) => Bun.write(filePath, data),
|
|
35215
35465
|
now: () => new Date,
|
|
@@ -35233,7 +35483,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
|
|
|
35233
35483
|
` + `# ${title}
|
|
35234
35484
|
|
|
35235
35485
|
`;
|
|
35236
|
-
const filePath =
|
|
35486
|
+
const filePath = path14.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
|
|
35237
35487
|
await deps.writeFile(filePath, frontmatter);
|
|
35238
35488
|
deps.logger.log(`Created new post: ${filePath}`);
|
|
35239
35489
|
return filePath;
|
|
@@ -35250,11 +35500,11 @@ function registerNewCommand(program2) {
|
|
|
35250
35500
|
}
|
|
35251
35501
|
|
|
35252
35502
|
// src/cli/commands/serve.ts
|
|
35253
|
-
import
|
|
35503
|
+
import path16 from "path";
|
|
35254
35504
|
|
|
35255
35505
|
// src/server.ts
|
|
35256
35506
|
import fs2 from "fs";
|
|
35257
|
-
import
|
|
35507
|
+
import path15 from "path";
|
|
35258
35508
|
async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
35259
35509
|
try {
|
|
35260
35510
|
const stats = await fs2.promises.stat(outputDir);
|
|
@@ -35289,18 +35539,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35289
35539
|
let filePath = "";
|
|
35290
35540
|
if (homePaginationMatch) {
|
|
35291
35541
|
const pageNumber = homePaginationMatch[1];
|
|
35292
|
-
filePath =
|
|
35542
|
+
filePath = path15.join(outputDir, "page", pageNumber, "index.html");
|
|
35293
35543
|
} else if (tagPaginationMatch) {
|
|
35294
35544
|
const tagSlug = tagPaginationMatch[1];
|
|
35295
35545
|
const pageNumber = tagPaginationMatch[2];
|
|
35296
|
-
filePath =
|
|
35546
|
+
filePath = path15.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
|
|
35297
35547
|
} else if (yearPaginationMatch) {
|
|
35298
35548
|
const year = yearPaginationMatch[1];
|
|
35299
35549
|
const pageNumber = yearPaginationMatch[2];
|
|
35300
|
-
filePath =
|
|
35550
|
+
filePath = path15.join(outputDir, year, "page", pageNumber, "index.html");
|
|
35301
35551
|
} else {
|
|
35302
|
-
const directPath =
|
|
35303
|
-
const withoutSlash =
|
|
35552
|
+
const directPath = path15.join(outputDir, pathname);
|
|
35553
|
+
const withoutSlash = path15.join(outputDir, pathname + ".html");
|
|
35304
35554
|
const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
|
|
35305
35555
|
const bunFileDirect = Bun.file(directPath);
|
|
35306
35556
|
const bunFileHtml = Bun.file(withHtml);
|
|
@@ -35309,7 +35559,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35309
35559
|
} else if (await bunFileHtml.exists()) {
|
|
35310
35560
|
filePath = withHtml;
|
|
35311
35561
|
} else {
|
|
35312
|
-
const indexPath =
|
|
35562
|
+
const indexPath = path15.join(outputDir, pathname, "index.html");
|
|
35313
35563
|
const bunFileIndex = Bun.file(indexPath);
|
|
35314
35564
|
if (await bunFileIndex.exists()) {
|
|
35315
35565
|
filePath = indexPath;
|
|
@@ -35323,7 +35573,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35323
35573
|
}
|
|
35324
35574
|
}
|
|
35325
35575
|
console.log(`Serving file: ${filePath}`);
|
|
35326
|
-
const extname =
|
|
35576
|
+
const extname = path15.extname(filePath);
|
|
35327
35577
|
let contentType = "text/html";
|
|
35328
35578
|
switch (extname) {
|
|
35329
35579
|
case ".js":
|
|
@@ -35382,7 +35632,7 @@ var defaultDeps5 = {
|
|
|
35382
35632
|
};
|
|
35383
35633
|
async function handleServeCommand(options2, deps = defaultDeps5) {
|
|
35384
35634
|
try {
|
|
35385
|
-
const outputDir =
|
|
35635
|
+
const outputDir = path16.resolve(options2.output);
|
|
35386
35636
|
const port = parseInt(options2.port, 10);
|
|
35387
35637
|
await deps.startServer(outputDir, port);
|
|
35388
35638
|
} catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -30961,6 +30961,20 @@ function detectFileConflicts(files) {
|
|
|
30961
30961
|
}
|
|
30962
30962
|
return errors;
|
|
30963
30963
|
}
|
|
30964
|
+
async function parseMarkdownFiles(filePaths, cdnConfig) {
|
|
30965
|
+
const resultsPromises = filePaths.map((filePath) => parseMarkdownFile(filePath, cdnConfig).then((result) => ({
|
|
30966
|
+
result,
|
|
30967
|
+
filePath
|
|
30968
|
+
})));
|
|
30969
|
+
const results = await Promise.all(resultsPromises);
|
|
30970
|
+
const postsWithPaths = [];
|
|
30971
|
+
for (const { result, filePath } of results) {
|
|
30972
|
+
if (result.post) {
|
|
30973
|
+
postsWithPaths.push({ post: result.post, filePath });
|
|
30974
|
+
}
|
|
30975
|
+
}
|
|
30976
|
+
return postsWithPaths;
|
|
30977
|
+
}
|
|
30964
30978
|
async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
|
|
30965
30979
|
try {
|
|
30966
30980
|
const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
|
|
@@ -31319,7 +31333,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
31319
31333
|
// src/site-generator.ts
|
|
31320
31334
|
var import_nunjucks2 = __toESM(require_nunjucks(), 1);
|
|
31321
31335
|
var import_slugify = __toESM(require_slugify(), 1);
|
|
31322
|
-
import
|
|
31336
|
+
import path9 from "path";
|
|
31323
31337
|
|
|
31324
31338
|
// src/utils/json-ld.ts
|
|
31325
31339
|
function generateOrganizationSchema(site) {
|
|
@@ -31485,6 +31499,165 @@ function generateHomePageSchemas(options2) {
|
|
|
31485
31499
|
return schemas;
|
|
31486
31500
|
}
|
|
31487
31501
|
|
|
31502
|
+
// src/utils/build-cache.ts
|
|
31503
|
+
var {hash } = globalThis.Bun;
|
|
31504
|
+
import path5 from "path";
|
|
31505
|
+
var CACHE_VERSION = "2.0.0";
|
|
31506
|
+
var CACHE_FILENAME = ".bunki-cache.json";
|
|
31507
|
+
async function hashFile(filePath) {
|
|
31508
|
+
try {
|
|
31509
|
+
const file = Bun.file(filePath);
|
|
31510
|
+
const content = await file.arrayBuffer();
|
|
31511
|
+
return hash(content).toString(36);
|
|
31512
|
+
} catch (error) {
|
|
31513
|
+
return "";
|
|
31514
|
+
}
|
|
31515
|
+
}
|
|
31516
|
+
async function getFileMtime(filePath) {
|
|
31517
|
+
try {
|
|
31518
|
+
const stat = await Bun.file(filePath).stat();
|
|
31519
|
+
return stat?.mtime?.getTime() || 0;
|
|
31520
|
+
} catch (error) {
|
|
31521
|
+
return 0;
|
|
31522
|
+
}
|
|
31523
|
+
}
|
|
31524
|
+
async function loadCache(cwd) {
|
|
31525
|
+
const cachePath = path5.join(cwd, CACHE_FILENAME);
|
|
31526
|
+
const cacheFile = Bun.file(cachePath);
|
|
31527
|
+
try {
|
|
31528
|
+
if (await cacheFile.exists()) {
|
|
31529
|
+
const content = await cacheFile.text();
|
|
31530
|
+
const cache = JSON.parse(content);
|
|
31531
|
+
if (cache.version !== CACHE_VERSION) {
|
|
31532
|
+
console.log(`Cache version mismatch (${cache.version} vs ${CACHE_VERSION}), rebuilding...`);
|
|
31533
|
+
return createEmptyCache();
|
|
31534
|
+
}
|
|
31535
|
+
return cache;
|
|
31536
|
+
}
|
|
31537
|
+
} catch (error) {
|
|
31538
|
+
console.warn("Error loading cache, rebuilding:", error);
|
|
31539
|
+
}
|
|
31540
|
+
return createEmptyCache();
|
|
31541
|
+
}
|
|
31542
|
+
async function saveCache(cwd, cache) {
|
|
31543
|
+
const cachePath = path5.join(cwd, CACHE_FILENAME);
|
|
31544
|
+
try {
|
|
31545
|
+
await Bun.write(cachePath, JSON.stringify(cache, null, 2));
|
|
31546
|
+
} catch (error) {
|
|
31547
|
+
console.warn("Error saving cache:", error);
|
|
31548
|
+
}
|
|
31549
|
+
}
|
|
31550
|
+
function createEmptyCache() {
|
|
31551
|
+
return {
|
|
31552
|
+
version: CACHE_VERSION,
|
|
31553
|
+
files: {}
|
|
31554
|
+
};
|
|
31555
|
+
}
|
|
31556
|
+
async function hasFileChanged(filePath, cache) {
|
|
31557
|
+
const cached = cache.files[filePath];
|
|
31558
|
+
if (!cached) {
|
|
31559
|
+
return true;
|
|
31560
|
+
}
|
|
31561
|
+
const currentMtime = await getFileMtime(filePath);
|
|
31562
|
+
if (currentMtime !== cached.mtime) {
|
|
31563
|
+
const currentHash = await hashFile(filePath);
|
|
31564
|
+
return currentHash !== cached.hash;
|
|
31565
|
+
}
|
|
31566
|
+
return false;
|
|
31567
|
+
}
|
|
31568
|
+
async function updateCacheEntry(filePath, cache, options2) {
|
|
31569
|
+
const currentHash = await hashFile(filePath);
|
|
31570
|
+
const currentMtime = await getFileMtime(filePath);
|
|
31571
|
+
cache.files[filePath] = {
|
|
31572
|
+
hash: currentHash,
|
|
31573
|
+
mtime: currentMtime,
|
|
31574
|
+
post: options2?.post,
|
|
31575
|
+
outputs: options2?.outputs
|
|
31576
|
+
};
|
|
31577
|
+
}
|
|
31578
|
+
async function hasConfigChanged(configPath, cache) {
|
|
31579
|
+
const currentHash = await hashFile(configPath);
|
|
31580
|
+
if (!cache.configHash) {
|
|
31581
|
+
cache.configHash = currentHash;
|
|
31582
|
+
return true;
|
|
31583
|
+
}
|
|
31584
|
+
if (currentHash !== cache.configHash) {
|
|
31585
|
+
cache.configHash = currentHash;
|
|
31586
|
+
return true;
|
|
31587
|
+
}
|
|
31588
|
+
return false;
|
|
31589
|
+
}
|
|
31590
|
+
function loadCachedPosts(cache, filePaths) {
|
|
31591
|
+
const posts = [];
|
|
31592
|
+
for (const filePath of filePaths) {
|
|
31593
|
+
const entry = cache.files[filePath];
|
|
31594
|
+
if (entry?.post) {
|
|
31595
|
+
posts.push(entry.post);
|
|
31596
|
+
}
|
|
31597
|
+
}
|
|
31598
|
+
return posts;
|
|
31599
|
+
}
|
|
31600
|
+
|
|
31601
|
+
// src/utils/change-detector.ts
|
|
31602
|
+
async function detectChanges(currentFiles, cache, options2 = {}) {
|
|
31603
|
+
const changes = {
|
|
31604
|
+
changedPosts: [],
|
|
31605
|
+
deletedPosts: [],
|
|
31606
|
+
stylesChanged: false,
|
|
31607
|
+
configChanged: false,
|
|
31608
|
+
templatesChanged: false,
|
|
31609
|
+
fullRebuild: false
|
|
31610
|
+
};
|
|
31611
|
+
if (options2.configPath) {
|
|
31612
|
+
const configChanged = await hasFileChanged(options2.configPath, cache);
|
|
31613
|
+
if (configChanged) {
|
|
31614
|
+
changes.configChanged = true;
|
|
31615
|
+
changes.fullRebuild = true;
|
|
31616
|
+
return changes;
|
|
31617
|
+
}
|
|
31618
|
+
}
|
|
31619
|
+
if (options2.templatePaths && options2.templatePaths.length > 0) {
|
|
31620
|
+
for (const templatePath of options2.templatePaths) {
|
|
31621
|
+
const changed = await hasFileChanged(templatePath, cache);
|
|
31622
|
+
if (changed) {
|
|
31623
|
+
changes.templatesChanged = true;
|
|
31624
|
+
changes.fullRebuild = true;
|
|
31625
|
+
return changes;
|
|
31626
|
+
}
|
|
31627
|
+
}
|
|
31628
|
+
}
|
|
31629
|
+
if (options2.stylesPaths && options2.stylesPaths.length > 0) {
|
|
31630
|
+
for (const stylePath of options2.stylesPaths) {
|
|
31631
|
+
const changed = await hasFileChanged(stylePath, cache);
|
|
31632
|
+
if (changed) {
|
|
31633
|
+
changes.stylesChanged = true;
|
|
31634
|
+
break;
|
|
31635
|
+
}
|
|
31636
|
+
}
|
|
31637
|
+
}
|
|
31638
|
+
for (const filePath of currentFiles) {
|
|
31639
|
+
const changed = await hasFileChanged(filePath, cache);
|
|
31640
|
+
if (changed) {
|
|
31641
|
+
changes.changedPosts.push(filePath);
|
|
31642
|
+
}
|
|
31643
|
+
}
|
|
31644
|
+
const cachedFiles = Object.keys(cache.files).filter((f) => f.endsWith(".md"));
|
|
31645
|
+
for (const cachedFile of cachedFiles) {
|
|
31646
|
+
if (!currentFiles.includes(cachedFile)) {
|
|
31647
|
+
changes.deletedPosts.push(cachedFile);
|
|
31648
|
+
}
|
|
31649
|
+
}
|
|
31650
|
+
if (changes.deletedPosts.length > 0) {
|
|
31651
|
+
changes.fullRebuild = true;
|
|
31652
|
+
}
|
|
31653
|
+
return changes;
|
|
31654
|
+
}
|
|
31655
|
+
function estimateTimeSaved(totalPosts, changedPosts) {
|
|
31656
|
+
const avgTimePerPost = 6;
|
|
31657
|
+
const skippedPosts = totalPosts - changedPosts;
|
|
31658
|
+
return skippedPosts * avgTimePerPost;
|
|
31659
|
+
}
|
|
31660
|
+
|
|
31488
31661
|
// src/utils/xml-builder.ts
|
|
31489
31662
|
function escapeXml(text) {
|
|
31490
31663
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -31717,14 +31890,14 @@ Sitemap: ${config.baseUrl}/sitemap.xml
|
|
|
31717
31890
|
|
|
31718
31891
|
// src/generators/pages.ts
|
|
31719
31892
|
var import_nunjucks = __toESM(require_nunjucks(), 1);
|
|
31720
|
-
import
|
|
31893
|
+
import path6 from "path";
|
|
31721
31894
|
function getSortedTags(tags, limit) {
|
|
31722
31895
|
const sorted = Object.values(tags).sort((a, b2) => b2.count - a.count);
|
|
31723
31896
|
return limit ? sorted.slice(0, limit) : sorted;
|
|
31724
31897
|
}
|
|
31725
31898
|
async function writeHtmlFile(outputDir, relativePath, content) {
|
|
31726
|
-
const fullPath =
|
|
31727
|
-
const dir =
|
|
31899
|
+
const fullPath = path6.join(outputDir, relativePath);
|
|
31900
|
+
const dir = path6.dirname(fullPath);
|
|
31728
31901
|
await ensureDir(dir);
|
|
31729
31902
|
await Bun.write(fullPath, content);
|
|
31730
31903
|
}
|
|
@@ -31900,12 +32073,12 @@ async function generateMapPage(site, config, outputDir) {
|
|
|
31900
32073
|
|
|
31901
32074
|
// src/generators/assets.ts
|
|
31902
32075
|
var {Glob: Glob2 } = globalThis.Bun;
|
|
31903
|
-
import
|
|
32076
|
+
import path8 from "path";
|
|
31904
32077
|
|
|
31905
32078
|
// src/utils/css-processor.ts
|
|
31906
32079
|
import { spawn } from "child_process";
|
|
31907
|
-
var {hash } = globalThis.Bun;
|
|
31908
|
-
import
|
|
32080
|
+
var {hash: hash2 } = globalThis.Bun;
|
|
32081
|
+
import path7 from "path";
|
|
31909
32082
|
async function processCSS(options2) {
|
|
31910
32083
|
const {
|
|
31911
32084
|
css,
|
|
@@ -31920,14 +32093,14 @@ async function processCSS(options2) {
|
|
|
31920
32093
|
}
|
|
31921
32094
|
return { outputPath: "" };
|
|
31922
32095
|
}
|
|
31923
|
-
const inputPath =
|
|
31924
|
-
const tempOutputPath =
|
|
31925
|
-
const postcssConfigPath = css.postcssConfig ?
|
|
32096
|
+
const inputPath = path7.resolve(projectRoot, css.input);
|
|
32097
|
+
const tempOutputPath = path7.resolve(outputDir, css.output);
|
|
32098
|
+
const postcssConfigPath = css.postcssConfig ? path7.resolve(projectRoot, css.postcssConfig) : path7.resolve(projectRoot, "postcss.config.js");
|
|
31926
32099
|
const inputFile = Bun.file(inputPath);
|
|
31927
32100
|
if (!await inputFile.exists()) {
|
|
31928
32101
|
throw new Error(`CSS input file not found: ${inputPath}`);
|
|
31929
32102
|
}
|
|
31930
|
-
const outputDirPath =
|
|
32103
|
+
const outputDirPath = path7.dirname(tempOutputPath);
|
|
31931
32104
|
await ensureDir(outputDirPath);
|
|
31932
32105
|
if (verbose) {
|
|
31933
32106
|
console.log("\uD83C\uDFA8 Building CSS with PostCSS...");
|
|
@@ -31939,12 +32112,12 @@ async function processCSS(options2) {
|
|
|
31939
32112
|
if (enableHashing) {
|
|
31940
32113
|
const cssFile = Bun.file(tempOutputPath);
|
|
31941
32114
|
const cssContent = await cssFile.arrayBuffer();
|
|
31942
|
-
const contentHash =
|
|
31943
|
-
const ext =
|
|
31944
|
-
const basename =
|
|
31945
|
-
const dir =
|
|
32115
|
+
const contentHash = hash2(cssContent).toString(36).slice(0, 8);
|
|
32116
|
+
const ext = path7.extname(tempOutputPath);
|
|
32117
|
+
const basename = path7.basename(tempOutputPath, ext);
|
|
32118
|
+
const dir = path7.dirname(tempOutputPath);
|
|
31946
32119
|
const hashedFilename = `${basename}.${contentHash}${ext}`;
|
|
31947
|
-
const hashedOutputPath =
|
|
32120
|
+
const hashedOutputPath = path7.join(dir, hashedFilename);
|
|
31948
32121
|
await Bun.write(hashedOutputPath, cssFile);
|
|
31949
32122
|
if (verbose) {
|
|
31950
32123
|
console.log(`\u2705 CSS hashed: ${hashedFilename}`);
|
|
@@ -32020,15 +32193,15 @@ async function generateStylesheet(config, outputDir) {
|
|
|
32020
32193
|
}
|
|
32021
32194
|
}
|
|
32022
32195
|
async function fallbackCSSGeneration(cssConfig, outputDir) {
|
|
32023
|
-
const cssFilePath =
|
|
32196
|
+
const cssFilePath = path8.resolve(process.cwd(), cssConfig.input);
|
|
32024
32197
|
const cssFile = Bun.file(cssFilePath);
|
|
32025
32198
|
if (!await cssFile.exists()) {
|
|
32026
32199
|
console.warn(`CSS input file not found: ${cssFilePath}`);
|
|
32027
32200
|
return;
|
|
32028
32201
|
}
|
|
32029
32202
|
try {
|
|
32030
|
-
const outputPath =
|
|
32031
|
-
const outputDirPath =
|
|
32203
|
+
const outputPath = path8.resolve(outputDir, cssConfig.output);
|
|
32204
|
+
const outputDirPath = path8.dirname(outputPath);
|
|
32032
32205
|
await ensureDir(outputDirPath);
|
|
32033
32206
|
await Bun.write(outputPath, cssFile);
|
|
32034
32207
|
console.log("\u2705 CSS file copied successfully (fallback mode)");
|
|
@@ -32037,19 +32210,19 @@ async function fallbackCSSGeneration(cssConfig, outputDir) {
|
|
|
32037
32210
|
}
|
|
32038
32211
|
}
|
|
32039
32212
|
async function copyStaticAssets(templatesDir, outputDir) {
|
|
32040
|
-
const assetsDir =
|
|
32041
|
-
const publicDir =
|
|
32213
|
+
const assetsDir = path8.join(templatesDir, "assets");
|
|
32214
|
+
const publicDir = path8.join(process.cwd(), "public");
|
|
32042
32215
|
if (await isDirectory(assetsDir)) {
|
|
32043
32216
|
const assetGlob = new Glob2("**/*.*");
|
|
32044
|
-
const assetsOutputDir =
|
|
32217
|
+
const assetsOutputDir = path8.join(outputDir, "assets");
|
|
32045
32218
|
await ensureDir(assetsOutputDir);
|
|
32046
32219
|
for await (const file of assetGlob.scan({
|
|
32047
32220
|
cwd: assetsDir,
|
|
32048
32221
|
absolute: true
|
|
32049
32222
|
})) {
|
|
32050
|
-
const relativePath =
|
|
32051
|
-
const targetPath =
|
|
32052
|
-
const targetDir =
|
|
32223
|
+
const relativePath = path8.relative(assetsDir, file);
|
|
32224
|
+
const targetPath = path8.join(assetsOutputDir, relativePath);
|
|
32225
|
+
const targetDir = path8.dirname(targetPath);
|
|
32053
32226
|
await ensureDir(targetDir);
|
|
32054
32227
|
await copyFile(file, targetPath);
|
|
32055
32228
|
}
|
|
@@ -32063,9 +32236,9 @@ async function copyStaticAssets(templatesDir, outputDir) {
|
|
|
32063
32236
|
})) {
|
|
32064
32237
|
if (await isDirectory(file))
|
|
32065
32238
|
continue;
|
|
32066
|
-
const relativePath =
|
|
32067
|
-
const destPath =
|
|
32068
|
-
const targetDir =
|
|
32239
|
+
const relativePath = path8.relative(publicDir, file);
|
|
32240
|
+
const destPath = path8.join(outputDir, relativePath);
|
|
32241
|
+
const targetDir = path8.dirname(destPath);
|
|
32069
32242
|
await ensureDir(targetDir);
|
|
32070
32243
|
await copyFile(file, destPath);
|
|
32071
32244
|
}
|
|
@@ -32147,6 +32320,8 @@ class SiteGenerator {
|
|
|
32147
32320
|
options;
|
|
32148
32321
|
site;
|
|
32149
32322
|
metrics;
|
|
32323
|
+
cache = null;
|
|
32324
|
+
incrementalMode = false;
|
|
32150
32325
|
constructor(options2) {
|
|
32151
32326
|
this.options = options2;
|
|
32152
32327
|
this.site = {
|
|
@@ -32192,6 +32367,9 @@ class SiteGenerator {
|
|
|
32192
32367
|
}
|
|
32193
32368
|
});
|
|
32194
32369
|
}
|
|
32370
|
+
enableIncrementalMode() {
|
|
32371
|
+
this.incrementalMode = true;
|
|
32372
|
+
}
|
|
32195
32373
|
async initialize() {
|
|
32196
32374
|
this.metrics.startStage("initialization");
|
|
32197
32375
|
console.log("Initializing site generator...");
|
|
@@ -32200,7 +32378,7 @@ class SiteGenerator {
|
|
|
32200
32378
|
setNoFollowExceptions(this.options.config.noFollowExceptions);
|
|
32201
32379
|
}
|
|
32202
32380
|
let tagDescriptions = {};
|
|
32203
|
-
const tagsTomlPath =
|
|
32381
|
+
const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
|
|
32204
32382
|
const tagsTomlFile = Bun.file(tagsTomlPath);
|
|
32205
32383
|
if (await tagsTomlFile.exists()) {
|
|
32206
32384
|
try {
|
|
@@ -32210,8 +32388,10 @@ class SiteGenerator {
|
|
|
32210
32388
|
console.warn("Error loading tag descriptions:", error);
|
|
32211
32389
|
}
|
|
32212
32390
|
}
|
|
32213
|
-
|
|
32214
|
-
|
|
32391
|
+
if (this.incrementalMode) {
|
|
32392
|
+
this.cache = await loadCache(process.cwd());
|
|
32393
|
+
}
|
|
32394
|
+
const posts = await this.parseContent();
|
|
32215
32395
|
const tags = {};
|
|
32216
32396
|
posts.forEach((post) => {
|
|
32217
32397
|
post.tagSlugs = {};
|
|
@@ -32249,7 +32429,21 @@ class SiteGenerator {
|
|
|
32249
32429
|
console.log("Generating static site...");
|
|
32250
32430
|
await ensureDir(this.options.outputDir);
|
|
32251
32431
|
this.metrics.startStage("cssProcessing");
|
|
32252
|
-
|
|
32432
|
+
let cssChanged = true;
|
|
32433
|
+
if (this.cache && this.incrementalMode && this.options.config.css) {
|
|
32434
|
+
const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
|
|
32435
|
+
const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
|
|
32436
|
+
const cssOutputExists = await Bun.file(cssOutputPath).exists();
|
|
32437
|
+
cssChanged = await hasFileChanged(cssInputPath, this.cache);
|
|
32438
|
+
if (!cssChanged && cssOutputExists) {
|
|
32439
|
+
console.log("\u23ED\uFE0F Skipping CSS (unchanged)");
|
|
32440
|
+
} else {
|
|
32441
|
+
await generateStylesheet(this.options.config, this.options.outputDir);
|
|
32442
|
+
await updateCacheEntry(cssInputPath, this.cache);
|
|
32443
|
+
}
|
|
32444
|
+
} else {
|
|
32445
|
+
await generateStylesheet(this.options.config, this.options.outputDir);
|
|
32446
|
+
}
|
|
32253
32447
|
this.metrics.startStage("pageGeneration");
|
|
32254
32448
|
await Promise.all([
|
|
32255
32449
|
generateIndexPages(this.site, this.options.config, this.options.outputDir),
|
|
@@ -32266,25 +32460,75 @@ class SiteGenerator {
|
|
|
32266
32460
|
const outputStats = await this.calculateOutputStats();
|
|
32267
32461
|
const buildMetrics = this.metrics.getMetrics(outputStats);
|
|
32268
32462
|
displayMetrics(buildMetrics);
|
|
32463
|
+
if (this.cache) {
|
|
32464
|
+
await saveCache(process.cwd(), this.cache);
|
|
32465
|
+
}
|
|
32269
32466
|
}
|
|
32270
32467
|
async generateFeeds() {
|
|
32271
32468
|
const pageSize = 10;
|
|
32272
32469
|
const rssContent = generateRSSFeed(this.site, this.options.config);
|
|
32273
|
-
await Bun.write(
|
|
32470
|
+
await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
|
|
32274
32471
|
const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
|
|
32275
|
-
await Bun.write(
|
|
32472
|
+
await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
|
|
32276
32473
|
console.log("Generated sitemap.xml");
|
|
32277
32474
|
const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
|
|
32278
32475
|
const sitemapSize = sitemapContent.length;
|
|
32279
32476
|
if (urlCount > 1000 || sitemapSize > 40000) {
|
|
32280
32477
|
const sitemapIndexContent = generateSitemapIndex(this.options.config);
|
|
32281
|
-
await Bun.write(
|
|
32478
|
+
await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
|
|
32282
32479
|
console.log("Generated sitemap_index.xml");
|
|
32283
32480
|
}
|
|
32284
32481
|
const robotsTxtContent = generateRobotsTxt(this.options.config);
|
|
32285
|
-
await Bun.write(
|
|
32482
|
+
await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
|
|
32286
32483
|
console.log("Generated robots.txt");
|
|
32287
32484
|
}
|
|
32485
|
+
async parseContent() {
|
|
32486
|
+
const strictMode = this.options.config.strictMode ?? false;
|
|
32487
|
+
if (!this.incrementalMode || !this.cache) {
|
|
32488
|
+
const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
|
|
32489
|
+
if (this.cache) {
|
|
32490
|
+
const allFiles2 = await findFilesByPattern("**/*.md", this.options.contentDir, true);
|
|
32491
|
+
const postsByPath = new Map(posts.map((p) => [p.url, p]));
|
|
32492
|
+
for (let i = 0;i < allFiles2.length; i++) {
|
|
32493
|
+
const filePath = allFiles2[i];
|
|
32494
|
+
const post = posts[i];
|
|
32495
|
+
await updateCacheEntry(filePath, this.cache, { post });
|
|
32496
|
+
}
|
|
32497
|
+
}
|
|
32498
|
+
return posts;
|
|
32499
|
+
}
|
|
32500
|
+
const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
|
|
32501
|
+
const configPath = path9.join(process.cwd(), "bunki.config.ts");
|
|
32502
|
+
const configChanged = await hasConfigChanged(configPath, this.cache);
|
|
32503
|
+
if (configChanged) {
|
|
32504
|
+
console.log("Config changed, full rebuild required");
|
|
32505
|
+
return this.parseContent();
|
|
32506
|
+
}
|
|
32507
|
+
const changes = await detectChanges(allFiles, this.cache);
|
|
32508
|
+
if (changes.fullRebuild) {
|
|
32509
|
+
console.log("Full rebuild required");
|
|
32510
|
+
this.incrementalMode = false;
|
|
32511
|
+
return this.parseContent();
|
|
32512
|
+
}
|
|
32513
|
+
if (changes.changedPosts.length === 0) {
|
|
32514
|
+
console.log("No content changes detected, using cached posts");
|
|
32515
|
+
const cachedPosts2 = loadCachedPosts(this.cache, allFiles);
|
|
32516
|
+
console.log(`\u2728 Loaded ${cachedPosts2.length} posts from cache (0ms parsing)`);
|
|
32517
|
+
return cachedPosts2;
|
|
32518
|
+
}
|
|
32519
|
+
const timeSaved = estimateTimeSaved(allFiles.length, changes.changedPosts.length);
|
|
32520
|
+
console.log(`\uD83D\uDCE6 Incremental build: ${changes.changedPosts.length}/${allFiles.length} files changed (~${timeSaved}ms saved)`);
|
|
32521
|
+
const changedPostsWithPaths = await parseMarkdownFiles(changes.changedPosts, this.options.config.cdn);
|
|
32522
|
+
const unchangedFiles = allFiles.filter((f) => !changes.changedPosts.includes(f));
|
|
32523
|
+
const cachedPosts = loadCachedPosts(this.cache, unchangedFiles);
|
|
32524
|
+
console.log(` Parsed: ${changedPostsWithPaths.length} new/changed, loaded: ${cachedPosts.length} from cache`);
|
|
32525
|
+
const changedPosts = changedPostsWithPaths.map((p) => p.post);
|
|
32526
|
+
const allPosts = [...changedPosts, ...cachedPosts].sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
|
|
32527
|
+
for (const { post, filePath } of changedPostsWithPaths) {
|
|
32528
|
+
await updateCacheEntry(filePath, this.cache, { post });
|
|
32529
|
+
}
|
|
32530
|
+
return allPosts;
|
|
32531
|
+
}
|
|
32288
32532
|
groupPostsByYear(posts) {
|
|
32289
32533
|
const postsByYear = {};
|
|
32290
32534
|
for (const post of posts) {
|
|
@@ -32324,11 +32568,11 @@ class SiteGenerator {
|
|
|
32324
32568
|
}
|
|
32325
32569
|
}
|
|
32326
32570
|
// src/utils/image-uploader.ts
|
|
32327
|
-
import
|
|
32571
|
+
import path11 from "path";
|
|
32328
32572
|
|
|
32329
32573
|
// src/utils/s3-uploader.ts
|
|
32330
32574
|
var {S3Client } = globalThis.Bun;
|
|
32331
|
-
import
|
|
32575
|
+
import path10 from "path";
|
|
32332
32576
|
|
|
32333
32577
|
class S3Uploader {
|
|
32334
32578
|
s3Config;
|
|
@@ -32451,8 +32695,8 @@ class S3Uploader {
|
|
|
32451
32695
|
let failedCount = 0;
|
|
32452
32696
|
const uploadTasks = imageFiles.map((imageFile) => async () => {
|
|
32453
32697
|
try {
|
|
32454
|
-
const imagePath =
|
|
32455
|
-
const filename =
|
|
32698
|
+
const imagePath = path10.join(imagesDir, imageFile);
|
|
32699
|
+
const filename = path10.basename(imagePath);
|
|
32456
32700
|
const file = Bun.file(imagePath);
|
|
32457
32701
|
const contentType = file.type;
|
|
32458
32702
|
if (process.env.BUNKI_DRY_RUN === "true") {} else {
|
|
@@ -32486,10 +32730,10 @@ function createUploader(config) {
|
|
|
32486
32730
|
}
|
|
32487
32731
|
|
|
32488
32732
|
// src/utils/image-uploader.ts
|
|
32489
|
-
var DEFAULT_IMAGES_DIR =
|
|
32733
|
+
var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
|
|
32490
32734
|
async function uploadImages(options2 = {}) {
|
|
32491
32735
|
try {
|
|
32492
|
-
const imagesDir =
|
|
32736
|
+
const imagesDir = path11.resolve(options2.images || DEFAULT_IMAGES_DIR);
|
|
32493
32737
|
if (!await fileExists(imagesDir)) {
|
|
32494
32738
|
console.log(`Creating images directory at ${imagesDir}...`);
|
|
32495
32739
|
await ensureDir(imagesDir);
|
|
@@ -32526,7 +32770,7 @@ async function uploadImages(options2 = {}) {
|
|
|
32526
32770
|
const uploader = createUploader(s3Config);
|
|
32527
32771
|
const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
|
|
32528
32772
|
if (options2.outputJson) {
|
|
32529
|
-
const outputFile =
|
|
32773
|
+
const outputFile = path11.resolve(options2.outputJson);
|
|
32530
32774
|
await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
|
|
32531
32775
|
console.log(`Image URL mapping saved to ${outputFile}`);
|
|
32532
32776
|
}
|
package/dist/parser.d.ts
CHANGED
|
@@ -4,4 +4,12 @@ export interface ParseResult {
|
|
|
4
4
|
posts: Post[];
|
|
5
5
|
errors: ParseError[];
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Parse specific markdown files (for incremental builds)
|
|
9
|
+
* Returns both posts and their file paths for cache updates
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseMarkdownFiles(filePaths: string[], cdnConfig?: CDNConfig): Promise<Array<{
|
|
12
|
+
post: Post;
|
|
13
|
+
filePath: string;
|
|
14
|
+
}>>;
|
|
7
15
|
export declare function parseMarkdownDirectory(contentDir: string, strictMode?: boolean, cdnConfig?: CDNConfig): Promise<Post[]>;
|
package/dist/site-generator.d.ts
CHANGED
|
@@ -7,7 +7,13 @@ export declare class SiteGenerator {
|
|
|
7
7
|
private options;
|
|
8
8
|
private site;
|
|
9
9
|
private metrics;
|
|
10
|
+
private cache;
|
|
11
|
+
private incrementalMode;
|
|
10
12
|
constructor(options: GeneratorOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Enable incremental builds
|
|
15
|
+
*/
|
|
16
|
+
enableIncrementalMode(): void;
|
|
11
17
|
/**
|
|
12
18
|
* Initialize site data - parse markdown and prepare site structure
|
|
13
19
|
*/
|
|
@@ -20,6 +26,10 @@ export declare class SiteGenerator {
|
|
|
20
26
|
* Generate all feed files (RSS, sitemap, robots.txt)
|
|
21
27
|
*/
|
|
22
28
|
private generateFeeds;
|
|
29
|
+
/**
|
|
30
|
+
* Parse content (full or incremental)
|
|
31
|
+
*/
|
|
32
|
+
private parseContent;
|
|
23
33
|
/**
|
|
24
34
|
* Group posts by year (Pacific timezone)
|
|
25
35
|
* @param posts - Array of posts
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build cache for incremental builds
|
|
3
|
+
* Tracks file hashes, modification times, parsed post data, and build outputs
|
|
4
|
+
*/
|
|
5
|
+
import type { Post } from "../types";
|
|
6
|
+
export interface CacheEntry {
|
|
7
|
+
/** Content hash of the file */
|
|
8
|
+
hash: string;
|
|
9
|
+
/** Last modification time (ms since epoch) */
|
|
10
|
+
mtime: number;
|
|
11
|
+
/** Cached parsed post data */
|
|
12
|
+
post?: Post;
|
|
13
|
+
/** Generated output files */
|
|
14
|
+
outputs?: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface BuildCache {
|
|
17
|
+
/** Version of cache format */
|
|
18
|
+
version: string;
|
|
19
|
+
/** File cache entries */
|
|
20
|
+
files: Record<string, CacheEntry>;
|
|
21
|
+
/** Config file hash */
|
|
22
|
+
configHash?: string;
|
|
23
|
+
/** Last full build timestamp */
|
|
24
|
+
lastFullBuild?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Calculate content hash for a file
|
|
28
|
+
*/
|
|
29
|
+
export declare function hashFile(filePath: string): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Get file modification time
|
|
32
|
+
*/
|
|
33
|
+
export declare function getFileMtime(filePath: string): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Load build cache from disk
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadCache(cwd: string): Promise<BuildCache>;
|
|
38
|
+
/**
|
|
39
|
+
* Save build cache to disk
|
|
40
|
+
*/
|
|
41
|
+
export declare function saveCache(cwd: string, cache: BuildCache): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Create empty build cache
|
|
44
|
+
*/
|
|
45
|
+
export declare function createEmptyCache(): BuildCache;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a file has changed since last build
|
|
48
|
+
*/
|
|
49
|
+
export declare function hasFileChanged(filePath: string, cache: BuildCache): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Update cache entry for a file
|
|
52
|
+
*/
|
|
53
|
+
export declare function updateCacheEntry(filePath: string, cache: BuildCache, options?: {
|
|
54
|
+
post?: Post;
|
|
55
|
+
outputs?: string[];
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Remove cache entry for a file
|
|
59
|
+
*/
|
|
60
|
+
export declare function removeCacheEntry(filePath: string, cache: BuildCache): void;
|
|
61
|
+
/**
|
|
62
|
+
* Check if config file has changed
|
|
63
|
+
*/
|
|
64
|
+
export declare function hasConfigChanged(configPath: string, cache: BuildCache): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Mark full build timestamp
|
|
67
|
+
*/
|
|
68
|
+
export declare function markFullBuild(cache: BuildCache): void;
|
|
69
|
+
/**
|
|
70
|
+
* Check if full rebuild is needed
|
|
71
|
+
*/
|
|
72
|
+
export declare function needsFullRebuild(cache: BuildCache, maxAge: number): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Load cached posts for files that haven't changed
|
|
75
|
+
*/
|
|
76
|
+
export declare function loadCachedPosts(cache: BuildCache, filePaths: string[]): Post[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change detection for incremental builds
|
|
3
|
+
* Determines which files have changed and what needs to be rebuilt
|
|
4
|
+
*/
|
|
5
|
+
import type { BuildCache } from "./build-cache";
|
|
6
|
+
import type { Post } from "../types";
|
|
7
|
+
export interface ChangeSet {
|
|
8
|
+
/** Posts that were added or modified */
|
|
9
|
+
changedPosts: string[];
|
|
10
|
+
/** Posts that were deleted */
|
|
11
|
+
deletedPosts: string[];
|
|
12
|
+
/** Whether any CSS/style files changed */
|
|
13
|
+
stylesChanged: boolean;
|
|
14
|
+
/** Whether config file changed */
|
|
15
|
+
configChanged: boolean;
|
|
16
|
+
/** Whether template files changed */
|
|
17
|
+
templatesChanged: boolean;
|
|
18
|
+
/** Whether a full rebuild is required */
|
|
19
|
+
fullRebuild: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Detect changes since last build
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectChanges(currentFiles: string[], cache: BuildCache, options?: {
|
|
25
|
+
configPath?: string;
|
|
26
|
+
stylesPaths?: string[];
|
|
27
|
+
templatePaths?: string[];
|
|
28
|
+
}): Promise<ChangeSet>;
|
|
29
|
+
/**
|
|
30
|
+
* Determine affected tags from changed posts
|
|
31
|
+
*/
|
|
32
|
+
export declare function getAffectedTags(changedPosts: Post[], allPosts: Post[]): Set<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Check if index pages need regeneration
|
|
35
|
+
*/
|
|
36
|
+
export declare function needsIndexRegeneration(changes: ChangeSet): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Estimate time saved by incremental build
|
|
39
|
+
*/
|
|
40
|
+
export declare function estimateTimeSaved(totalPosts: number, changedPosts: number): number;
|
package/package.json
CHANGED