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/README.md +144 -8
- package/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli.js +728 -499
- package/dist/index.js +288 -45
- 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
- package/dist/cli/commands/templates/archive-njk.d.ts +0 -4
- package/dist/cli/commands/templates/base-njk.d.ts +0 -4
- package/dist/cli/commands/templates/default-css.d.ts +0 -4
- package/dist/cli/commands/templates/index-njk.d.ts +0 -4
- package/dist/cli/commands/templates/index.d.ts +0 -14
- package/dist/cli/commands/templates/post-njk.d.ts +0 -4
- package/dist/cli/commands/templates/sample-post.d.ts +0 -4
- package/dist/cli/commands/templates/tag-njk.d.ts +0 -4
- package/dist/cli/commands/templates/tags-njk.d.ts +0 -4
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 = {
|
|
@@ -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 =
|
|
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
|
-
|
|
32214
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
32570
|
+
import path11 from "path";
|
|
32328
32571
|
|
|
32329
32572
|
// src/utils/s3-uploader.ts
|
|
32330
32573
|
var {S3Client } = globalThis.Bun;
|
|
32331
|
-
import
|
|
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 =
|
|
32455
|
-
const filename =
|
|
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 =
|
|
32732
|
+
var DEFAULT_IMAGES_DIR = path11.join(process.cwd(), "assets");
|
|
32490
32733
|
async function uploadImages(options2 = {}) {
|
|
32491
32734
|
try {
|
|
32492
|
-
const imagesDir =
|
|
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 =
|
|
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[]>;
|
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