bunki 0.17.0 → 0.18.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/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 path8 from "path";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -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 path5 from "path";
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 = path5.join(outputDir, relativePath);
31727
- const dir = path5.dirname(fullPath);
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 path7 from "path";
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 path6 from "path";
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 = path6.resolve(projectRoot, css.input);
31924
- const tempOutputPath = path6.resolve(outputDir, css.output);
31925
- const postcssConfigPath = css.postcssConfig ? path6.resolve(projectRoot, css.postcssConfig) : path6.resolve(projectRoot, "postcss.config.js");
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 = path6.dirname(tempOutputPath);
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 = hash(cssContent).toString(36).slice(0, 8);
31943
- const ext = path6.extname(tempOutputPath);
31944
- const basename = path6.basename(tempOutputPath, ext);
31945
- const dir = path6.dirname(tempOutputPath);
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 = path6.join(dir, hashedFilename);
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 = path7.resolve(process.cwd(), cssConfig.input);
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 = path7.resolve(outputDir, cssConfig.output);
32031
- const outputDirPath = path7.dirname(outputPath);
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 = path7.join(templatesDir, "assets");
32041
- const publicDir = path7.join(process.cwd(), "public");
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 = path7.join(outputDir, "assets");
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 = path7.relative(assetsDir, file);
32051
- const targetPath = path7.join(assetsOutputDir, relativePath);
32052
- const targetDir = path7.dirname(targetPath);
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 = path7.relative(publicDir, file);
32067
- const destPath = path7.join(outputDir, relativePath);
32068
- const targetDir = path7.dirname(destPath);
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 = {
@@ -32158,8 +32333,7 @@ class SiteGenerator {
32158
32333
  this.metrics = new MetricsCollector;
32159
32334
  const env = import_nunjucks2.default.configure(this.options.templatesDir, {
32160
32335
  autoescape: true,
32161
- watch: false,
32162
- noCache: false
32336
+ watch: false
32163
32337
  });
32164
32338
  env.addFilter("date", (date, format) => {
32165
32339
  const d2 = toPacificTime(date);
@@ -32192,6 +32366,9 @@ class SiteGenerator {
32192
32366
  }
32193
32367
  });
32194
32368
  }
32369
+ enableIncrementalMode() {
32370
+ this.incrementalMode = true;
32371
+ }
32195
32372
  async initialize() {
32196
32373
  this.metrics.startStage("initialization");
32197
32374
  console.log("Initializing site generator...");
@@ -32200,7 +32377,7 @@ class SiteGenerator {
32200
32377
  setNoFollowExceptions(this.options.config.noFollowExceptions);
32201
32378
  }
32202
32379
  let tagDescriptions = {};
32203
- const tagsTomlPath = path8.join(process.cwd(), "src", "tags.toml");
32380
+ const tagsTomlPath = path9.join(process.cwd(), "src", "tags.toml");
32204
32381
  const tagsTomlFile = Bun.file(tagsTomlPath);
32205
32382
  if (await tagsTomlFile.exists()) {
32206
32383
  try {
@@ -32210,8 +32387,10 @@ class SiteGenerator {
32210
32387
  console.warn("Error loading tag descriptions:", error);
32211
32388
  }
32212
32389
  }
32213
- const strictMode = this.options.config.strictMode ?? false;
32214
- const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
32390
+ if (this.incrementalMode) {
32391
+ this.cache = await loadCache(process.cwd());
32392
+ }
32393
+ const posts = await this.parseContent();
32215
32394
  const tags = {};
32216
32395
  posts.forEach((post) => {
32217
32396
  post.tagSlugs = {};
@@ -32249,7 +32428,21 @@ class SiteGenerator {
32249
32428
  console.log("Generating static site...");
32250
32429
  await ensureDir(this.options.outputDir);
32251
32430
  this.metrics.startStage("cssProcessing");
32252
- await generateStylesheet(this.options.config, this.options.outputDir);
32431
+ let cssChanged = true;
32432
+ if (this.cache && this.incrementalMode && this.options.config.css) {
32433
+ const cssInputPath = path9.resolve(process.cwd(), this.options.config.css.input);
32434
+ const cssOutputPath = path9.join(this.options.outputDir, this.options.config.css.output);
32435
+ const cssOutputExists = await Bun.file(cssOutputPath).exists();
32436
+ cssChanged = await hasFileChanged(cssInputPath, this.cache);
32437
+ if (!cssChanged && cssOutputExists) {
32438
+ console.log("\u23ED\uFE0F Skipping CSS (unchanged)");
32439
+ } else {
32440
+ await generateStylesheet(this.options.config, this.options.outputDir);
32441
+ await updateCacheEntry(cssInputPath, this.cache);
32442
+ }
32443
+ } else {
32444
+ await generateStylesheet(this.options.config, this.options.outputDir);
32445
+ }
32253
32446
  this.metrics.startStage("pageGeneration");
32254
32447
  await Promise.all([
32255
32448
  generateIndexPages(this.site, this.options.config, this.options.outputDir),
@@ -32266,25 +32459,75 @@ class SiteGenerator {
32266
32459
  const outputStats = await this.calculateOutputStats();
32267
32460
  const buildMetrics = this.metrics.getMetrics(outputStats);
32268
32461
  displayMetrics(buildMetrics);
32462
+ if (this.cache) {
32463
+ await saveCache(process.cwd(), this.cache);
32464
+ }
32269
32465
  }
32270
32466
  async generateFeeds() {
32271
32467
  const pageSize = 10;
32272
32468
  const rssContent = generateRSSFeed(this.site, this.options.config);
32273
- await Bun.write(path8.join(this.options.outputDir, "feed.xml"), rssContent);
32469
+ await Bun.write(path9.join(this.options.outputDir, "feed.xml"), rssContent);
32274
32470
  const sitemapContent = generateSitemap(this.site, this.options.config, pageSize);
32275
- await Bun.write(path8.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32471
+ await Bun.write(path9.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
32276
32472
  console.log("Generated sitemap.xml");
32277
32473
  const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
32278
32474
  const sitemapSize = sitemapContent.length;
32279
32475
  if (urlCount > 1000 || sitemapSize > 40000) {
32280
32476
  const sitemapIndexContent = generateSitemapIndex(this.options.config);
32281
- await Bun.write(path8.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32477
+ await Bun.write(path9.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
32282
32478
  console.log("Generated sitemap_index.xml");
32283
32479
  }
32284
32480
  const robotsTxtContent = generateRobotsTxt(this.options.config);
32285
- await Bun.write(path8.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32481
+ await Bun.write(path9.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
32286
32482
  console.log("Generated robots.txt");
32287
32483
  }
32484
+ async parseContent() {
32485
+ const strictMode = this.options.config.strictMode ?? false;
32486
+ if (!this.incrementalMode || !this.cache) {
32487
+ const posts = await parseMarkdownDirectory(this.options.contentDir, strictMode, this.options.config.cdn);
32488
+ if (this.cache) {
32489
+ const allFiles2 = await findFilesByPattern("**/*.md", this.options.contentDir, true);
32490
+ const postsByPath = new Map(posts.map((p) => [p.url, p]));
32491
+ for (let i = 0;i < allFiles2.length; i++) {
32492
+ const filePath = allFiles2[i];
32493
+ const post = posts[i];
32494
+ await updateCacheEntry(filePath, this.cache, { post });
32495
+ }
32496
+ }
32497
+ return posts;
32498
+ }
32499
+ const allFiles = await findFilesByPattern("**/*.md", this.options.contentDir, true);
32500
+ const configPath = path9.join(process.cwd(), "bunki.config.ts");
32501
+ const configChanged = await hasConfigChanged(configPath, this.cache);
32502
+ if (configChanged) {
32503
+ console.log("Config changed, full rebuild required");
32504
+ return this.parseContent();
32505
+ }
32506
+ const changes = await detectChanges(allFiles, this.cache);
32507
+ if (changes.fullRebuild) {
32508
+ console.log("Full rebuild required");
32509
+ this.incrementalMode = false;
32510
+ return this.parseContent();
32511
+ }
32512
+ if (changes.changedPosts.length === 0) {
32513
+ console.log("No content changes detected, using cached posts");
32514
+ const cachedPosts2 = loadCachedPosts(this.cache, allFiles);
32515
+ console.log(`\u2728 Loaded ${cachedPosts2.length} posts from cache (0ms parsing)`);
32516
+ return cachedPosts2;
32517
+ }
32518
+ const timeSaved = estimateTimeSaved(allFiles.length, changes.changedPosts.length);
32519
+ console.log(`\uD83D\uDCE6 Incremental build: ${changes.changedPosts.length}/${allFiles.length} files changed (~${timeSaved}ms saved)`);
32520
+ const changedPostsWithPaths = await parseMarkdownFiles(changes.changedPosts, this.options.config.cdn);
32521
+ const unchangedFiles = allFiles.filter((f) => !changes.changedPosts.includes(f));
32522
+ const cachedPosts = loadCachedPosts(this.cache, unchangedFiles);
32523
+ console.log(` Parsed: ${changedPostsWithPaths.length} new/changed, loaded: ${cachedPosts.length} from cache`);
32524
+ const changedPosts = changedPostsWithPaths.map((p) => p.post);
32525
+ const allPosts = [...changedPosts, ...cachedPosts].sort((a, b2) => new Date(b2.date).getTime() - new Date(a.date).getTime());
32526
+ for (const { post, filePath } of changedPostsWithPaths) {
32527
+ await updateCacheEntry(filePath, this.cache, { post });
32528
+ }
32529
+ return allPosts;
32530
+ }
32288
32531
  groupPostsByYear(posts) {
32289
32532
  const postsByYear = {};
32290
32533
  for (const post of posts) {
@@ -32324,11 +32567,11 @@ class SiteGenerator {
32324
32567
  }
32325
32568
  }
32326
32569
  // src/utils/image-uploader.ts
32327
- import path10 from "path";
32570
+ import path11 from "path";
32328
32571
 
32329
32572
  // src/utils/s3-uploader.ts
32330
32573
  var {S3Client } = globalThis.Bun;
32331
- import path9 from "path";
32574
+ import path10 from "path";
32332
32575
 
32333
32576
  class S3Uploader {
32334
32577
  s3Config;
@@ -32451,8 +32694,8 @@ class S3Uploader {
32451
32694
  let failedCount = 0;
32452
32695
  const uploadTasks = imageFiles.map((imageFile) => async () => {
32453
32696
  try {
32454
- const imagePath = path9.join(imagesDir, imageFile);
32455
- const filename = path9.basename(imagePath);
32697
+ const imagePath = path10.join(imagesDir, imageFile);
32698
+ const filename = path10.basename(imagePath);
32456
32699
  const file = Bun.file(imagePath);
32457
32700
  const contentType = file.type;
32458
32701
  if (process.env.BUNKI_DRY_RUN === "true") {} else {
@@ -32486,10 +32729,10 @@ function createUploader(config) {
32486
32729
  }
32487
32730
 
32488
32731
  // src/utils/image-uploader.ts
32489
- var DEFAULT_IMAGES_DIR = path10.join(process.cwd(), "assets");
32732
+ var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
32490
32733
  async function uploadImages(options2 = {}) {
32491
32734
  try {
32492
- const imagesDir = path10.resolve(options2.images || DEFAULT_IMAGES_DIR);
32735
+ const imagesDir = path11.resolve(options2.images || DEFAULT_IMAGES_DIR);
32493
32736
  if (!await fileExists(imagesDir)) {
32494
32737
  console.log(`Creating images directory at ${imagesDir}...`);
32495
32738
  await ensureDir(imagesDir);
@@ -32526,7 +32769,7 @@ async function uploadImages(options2 = {}) {
32526
32769
  const uploader = createUploader(s3Config);
32527
32770
  const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
32528
32771
  if (options2.outputJson) {
32529
- const outputFile = path10.resolve(options2.outputJson);
32772
+ const outputFile = path11.resolve(options2.outputJson);
32530
32773
  await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
32531
32774
  console.log(`Image URL mapping saved to ${outputFile}`);
32532
32775
  }
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[]>;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunki",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "An opinionated static site generator built with Bun featuring PostCSS integration and modern web development workflows",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",