bunki 0.16.0 → 0.16.2
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 +68 -5
- package/dist/cli.js +146 -72
- package/dist/index.js +386 -318
- package/dist/utils/file-utils.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,13 +67,31 @@ export default (): SiteConfig => ({
|
|
|
67
67
|
|
|
68
68
|
## Content & Frontmatter
|
|
69
69
|
|
|
70
|
-
Create Markdown files in `content/YYYY/`
|
|
70
|
+
Create Markdown files in `content/YYYY/` using either pattern:
|
|
71
|
+
|
|
72
|
+
**Option 1: Single file** (traditional)
|
|
73
|
+
```
|
|
74
|
+
content/2025/my-post.md
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Option 2: Directory with README** (Obsidian-friendly)
|
|
78
|
+
```
|
|
79
|
+
content/2025/my-post/README.md
|
|
80
|
+
content/2025/my-post/image.jpg
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Both patterns generate the same output: `dist/2025/my-post/index.html`
|
|
84
|
+
|
|
85
|
+
> [!WARNING]
|
|
86
|
+
> You cannot have both patterns for the same slug. Bunki will throw a validation error if both `content/2025/my-post.md` AND `content/2025/my-post/README.md` exist.
|
|
87
|
+
|
|
88
|
+
Example frontmatter:
|
|
71
89
|
|
|
72
90
|
```markdown
|
|
73
91
|
---
|
|
74
92
|
title: "Post Title"
|
|
75
93
|
date: 2025-01-15T09:00:00-07:00
|
|
76
|
-
tags: [web, performance]
|
|
94
|
+
tags: [web-development, performance-optimization]
|
|
77
95
|
excerpt: "Optional summary for listings"
|
|
78
96
|
---
|
|
79
97
|
|
|
@@ -89,13 +107,47 @@ Your content here with **markdown** support.
|
|
|
89
107
|
</video>
|
|
90
108
|
```
|
|
91
109
|
|
|
110
|
+
### Tag Format
|
|
111
|
+
|
|
112
|
+
> [!IMPORTANT]
|
|
113
|
+
> Tags must use hyphens instead of spaces: `web-development` NOT `"web development"`
|
|
114
|
+
|
|
115
|
+
Tags with spaces will fail validation. Use hyphenated slugs:
|
|
116
|
+
- ✅ `tags: [new-york-city, travel, family-friendly]`
|
|
117
|
+
- ❌ `tags: ["new york city", "travel", "family friendly"]`
|
|
118
|
+
|
|
92
119
|
Optional: Define tag descriptions in `src/tags.toml`:
|
|
93
120
|
|
|
94
121
|
```toml
|
|
95
|
-
performance = "Performance optimization and speed"
|
|
96
|
-
web = "Web development and technology"
|
|
122
|
+
performance-optimization = "Performance optimization and speed"
|
|
123
|
+
web-development = "Web development and technology"
|
|
124
|
+
new-york-city = "New York City travel guides"
|
|
97
125
|
```
|
|
98
126
|
|
|
127
|
+
### Internal Links (Relative Markdown Links)
|
|
128
|
+
|
|
129
|
+
Bunki automatically converts relative markdown links to absolute URLs during build time. This lets you write cross-references using familiar file paths:
|
|
130
|
+
|
|
131
|
+
**In your markdown:**
|
|
132
|
+
```markdown
|
|
133
|
+
Check out [my earlier post](../2023/introduction.md) for context.
|
|
134
|
+
|
|
135
|
+
See also [related article](../../2020/old-post.md).
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Generated HTML:**
|
|
139
|
+
```html
|
|
140
|
+
<a href="/2023/introduction/">my earlier post</a>
|
|
141
|
+
<a href="/2020/old-post/">related article</a>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This feature works with:
|
|
145
|
+
- `../YEAR/slug.md` - Single level up
|
|
146
|
+
- `../../YEAR/slug.md` - Multiple levels up
|
|
147
|
+
- Any number of `../` sequences
|
|
148
|
+
|
|
149
|
+
The links are automatically converted to absolute URLs (`/YEAR/slug/`) that match your site's URL structure.
|
|
150
|
+
|
|
99
151
|
### Business Location Data
|
|
100
152
|
|
|
101
153
|
Add structured business/location data with automatic validation:
|
|
@@ -773,6 +825,7 @@ dist/
|
|
|
773
825
|
## Features
|
|
774
826
|
|
|
775
827
|
- **Markdown Processing**: Frontmatter extraction, code highlighting, HTML sanitization
|
|
828
|
+
- **Relative Link Conversion**: Automatic conversion of relative markdown links (`../2023/post.md`) to absolute URLs (`/2023/post/`)
|
|
776
829
|
- **Frontmatter Validation**: Automatic validation of business location data with clear error messages
|
|
777
830
|
- **Security**: XSS protection, sanitized HTML, link hardening
|
|
778
831
|
- **Performance**: Static files, optional gzip, optimized output
|
|
@@ -819,7 +872,17 @@ bunki/
|
|
|
819
872
|
|
|
820
873
|
## Changelog
|
|
821
874
|
|
|
822
|
-
### v0.
|
|
875
|
+
### v0.16.0 (Current)
|
|
876
|
+
|
|
877
|
+
- **Relative Link Conversion**: Automatically convert relative markdown links to absolute URLs
|
|
878
|
+
- Supports `../2023/post.md` → `/2023/post/` conversion during build time
|
|
879
|
+
- Works with multiple parent directories (`../../`, `../../../`, etc.)
|
|
880
|
+
- Preserves link text and formatting
|
|
881
|
+
- Enables cleaner internal cross-references in markdown files
|
|
882
|
+
- **Comprehensive Testing**: Added 13 new tests for relative link conversion
|
|
883
|
+
- **Zero Configuration**: Works automatically without any setup required
|
|
884
|
+
|
|
885
|
+
### v0.15.0
|
|
823
886
|
|
|
824
887
|
- **Frontmatter Validation**: Automatic validation of business location data
|
|
825
888
|
- Enforces `business:` field (rejects deprecated `location:`)
|
package/dist/cli.js
CHANGED
|
@@ -28370,13 +28370,16 @@ function registerCssCommand(program2) {
|
|
|
28370
28370
|
}
|
|
28371
28371
|
|
|
28372
28372
|
// src/cli/commands/generate.ts
|
|
28373
|
-
import
|
|
28373
|
+
import path7 from "path";
|
|
28374
28374
|
|
|
28375
28375
|
// src/site-generator.ts
|
|
28376
28376
|
var import_nunjucks = __toESM(require_nunjucks(), 1);
|
|
28377
28377
|
var import_slugify = __toESM(require_slugify(), 1);
|
|
28378
28378
|
var {Glob: Glob2 } = globalThis.Bun;
|
|
28379
28379
|
import fs2 from "fs";
|
|
28380
|
+
import path6 from "path";
|
|
28381
|
+
|
|
28382
|
+
// src/parser.ts
|
|
28380
28383
|
import path5 from "path";
|
|
28381
28384
|
|
|
28382
28385
|
// src/utils/file-utils.ts
|
|
@@ -28423,7 +28426,12 @@ async function readFileAsText(filePath) {
|
|
|
28423
28426
|
}
|
|
28424
28427
|
}
|
|
28425
28428
|
function getBaseFilename(filePath, extension = ".md") {
|
|
28426
|
-
|
|
28429
|
+
const basename = path4.basename(filePath, extension);
|
|
28430
|
+
if (basename.toLowerCase() === "readme") {
|
|
28431
|
+
const dir = path4.dirname(filePath);
|
|
28432
|
+
return path4.basename(dir);
|
|
28433
|
+
}
|
|
28434
|
+
return basename;
|
|
28427
28435
|
}
|
|
28428
28436
|
async function createDir(dirPath) {
|
|
28429
28437
|
try {
|
|
@@ -32951,6 +32959,11 @@ function createMarked(cdnConfig) {
|
|
|
32951
32959
|
walkTokens(token) {
|
|
32952
32960
|
if (token.type === "link") {
|
|
32953
32961
|
token.href = token.href || "";
|
|
32962
|
+
let relativeMatch = token.href.match(/^(\.\.\/)+(\d{4})\/([a-zA-Z0-9_-]+?)(?:\.md)?(?:\/)?$/);
|
|
32963
|
+
if (relativeMatch) {
|
|
32964
|
+
const [, , year, slug] = relativeMatch;
|
|
32965
|
+
token.href = `/${year}/${slug}/`;
|
|
32966
|
+
}
|
|
32954
32967
|
const isExternal = token.href && (token.href.startsWith("http://") || token.href.startsWith("https://") || token.href.startsWith("//"));
|
|
32955
32968
|
if (isExternal) {
|
|
32956
32969
|
token.isExternalLink = true;
|
|
@@ -33214,6 +33227,20 @@ async function parseMarkdownFile(filePath, cdnConfig) {
|
|
|
33214
33227
|
};
|
|
33215
33228
|
}
|
|
33216
33229
|
}
|
|
33230
|
+
if (data.tags && Array.isArray(data.tags)) {
|
|
33231
|
+
const tagsWithSpaces = data.tags.filter((tag) => tag.includes(" "));
|
|
33232
|
+
if (tagsWithSpaces.length > 0) {
|
|
33233
|
+
return {
|
|
33234
|
+
post: null,
|
|
33235
|
+
error: {
|
|
33236
|
+
file: filePath,
|
|
33237
|
+
type: "validation",
|
|
33238
|
+
message: `Tags must not contain spaces. Found: ${tagsWithSpaces.map((t) => `"${t}"`).join(", ")}`,
|
|
33239
|
+
suggestion: `Use hyphens instead of spaces. Example: "new-york-city" instead of "new york city"`
|
|
33240
|
+
}
|
|
33241
|
+
};
|
|
33242
|
+
}
|
|
33243
|
+
}
|
|
33217
33244
|
let slug = getBaseFilename(filePath);
|
|
33218
33245
|
const sanitizedHtml = convertMarkdownToHtml(content, cdnConfig);
|
|
33219
33246
|
const pacificDate = toPacificTime(data.date);
|
|
@@ -33277,14 +33304,55 @@ async function parseMarkdownFile(filePath, cdnConfig) {
|
|
|
33277
33304
|
}
|
|
33278
33305
|
|
|
33279
33306
|
// src/parser.ts
|
|
33307
|
+
function detectFileConflicts(files) {
|
|
33308
|
+
const errors = [];
|
|
33309
|
+
const slugMap = new Map;
|
|
33310
|
+
for (const filePath of files) {
|
|
33311
|
+
const slug = getBaseFilename(filePath);
|
|
33312
|
+
const dir = path5.dirname(filePath);
|
|
33313
|
+
const year = path5.basename(dir);
|
|
33314
|
+
const key = `${year}/${slug}`;
|
|
33315
|
+
if (!slugMap.has(key)) {
|
|
33316
|
+
slugMap.set(key, []);
|
|
33317
|
+
}
|
|
33318
|
+
slugMap.get(key).push(filePath);
|
|
33319
|
+
}
|
|
33320
|
+
for (const [key, paths] of slugMap.entries()) {
|
|
33321
|
+
if (paths.length > 1) {
|
|
33322
|
+
errors.push({
|
|
33323
|
+
file: paths[0],
|
|
33324
|
+
type: "validation",
|
|
33325
|
+
message: `Conflicting files for '${key}': ${paths.map((p) => path5.relative(process.cwd(), p)).join(" AND ")}`,
|
|
33326
|
+
suggestion: `Remove one of the files. Keep either the .md file OR the /README.md file, not both.`
|
|
33327
|
+
});
|
|
33328
|
+
}
|
|
33329
|
+
}
|
|
33330
|
+
return errors;
|
|
33331
|
+
}
|
|
33280
33332
|
async function parseMarkdownDirectory(contentDir, strictMode = false, cdnConfig) {
|
|
33281
33333
|
try {
|
|
33282
33334
|
const markdownFiles = await findFilesByPattern("**/*.md", contentDir, true);
|
|
33283
33335
|
console.log(`Found ${markdownFiles.length} markdown files`);
|
|
33336
|
+
const conflictErrors = detectFileConflicts(markdownFiles);
|
|
33337
|
+
if (conflictErrors.length > 0) {
|
|
33338
|
+
console.error(`
|
|
33339
|
+
\u26A0\uFE0F Found ${conflictErrors.length} file conflict(s):
|
|
33340
|
+
`);
|
|
33341
|
+
conflictErrors.forEach((e) => {
|
|
33342
|
+
console.error(` \u274C ${e.message}`);
|
|
33343
|
+
if (e.suggestion) {
|
|
33344
|
+
console.error(` \uD83D\uDCA1 ${e.suggestion}`);
|
|
33345
|
+
}
|
|
33346
|
+
});
|
|
33347
|
+
console.error("");
|
|
33348
|
+
if (strictMode) {
|
|
33349
|
+
throw new Error(`File conflicts detected. Fix conflicts before building.`);
|
|
33350
|
+
}
|
|
33351
|
+
}
|
|
33284
33352
|
const resultsPromises = markdownFiles.map((filePath) => parseMarkdownFile(filePath, cdnConfig));
|
|
33285
33353
|
const results = await Promise.all(resultsPromises);
|
|
33286
33354
|
const posts = [];
|
|
33287
|
-
const errors = [];
|
|
33355
|
+
const errors = [...conflictErrors];
|
|
33288
33356
|
for (const result of results) {
|
|
33289
33357
|
if (result.post) {
|
|
33290
33358
|
posts.push(result.post);
|
|
@@ -33618,7 +33686,7 @@ class SiteGenerator {
|
|
|
33618
33686
|
setNoFollowExceptions(this.options.config.noFollowExceptions);
|
|
33619
33687
|
}
|
|
33620
33688
|
let tagDescriptions = {};
|
|
33621
|
-
const tagsTomlPath =
|
|
33689
|
+
const tagsTomlPath = path6.join(process.cwd(), "src", "tags.toml");
|
|
33622
33690
|
const tagsTomlFile = Bun.file(tagsTomlPath);
|
|
33623
33691
|
if (await tagsTomlFile.exists()) {
|
|
33624
33692
|
try {
|
|
@@ -33686,7 +33754,7 @@ class SiteGenerator {
|
|
|
33686
33754
|
const notFoundHtml = import_nunjucks.default.render("404.njk", {
|
|
33687
33755
|
site: this.options.config
|
|
33688
33756
|
});
|
|
33689
|
-
await Bun.write(
|
|
33757
|
+
await Bun.write(path6.join(this.options.outputDir, "404.html"), notFoundHtml);
|
|
33690
33758
|
console.log("Generated 404.html");
|
|
33691
33759
|
} catch (error) {
|
|
33692
33760
|
if (error instanceof Error && error.message.includes("404.njk")) {
|
|
@@ -33698,7 +33766,7 @@ class SiteGenerator {
|
|
|
33698
33766
|
}
|
|
33699
33767
|
async generateYearArchives() {
|
|
33700
33768
|
for (const [year, yearPosts] of Object.entries(this.site.postsByYear)) {
|
|
33701
|
-
const yearDir =
|
|
33769
|
+
const yearDir = path6.join(this.options.outputDir, year);
|
|
33702
33770
|
await ensureDir(yearDir);
|
|
33703
33771
|
const pageSize = 10;
|
|
33704
33772
|
const totalPages = Math.ceil(yearPosts.length / pageSize);
|
|
@@ -33737,11 +33805,11 @@ class SiteGenerator {
|
|
|
33737
33805
|
jsonLd
|
|
33738
33806
|
});
|
|
33739
33807
|
if (page === 1) {
|
|
33740
|
-
await Bun.write(
|
|
33808
|
+
await Bun.write(path6.join(yearDir, "index.html"), yearPageHtml);
|
|
33741
33809
|
} else {
|
|
33742
|
-
const pageDir =
|
|
33810
|
+
const pageDir = path6.join(yearDir, "page", page.toString());
|
|
33743
33811
|
await ensureDir(pageDir);
|
|
33744
|
-
await Bun.write(
|
|
33812
|
+
await Bun.write(path6.join(pageDir, "index.html"), yearPageHtml);
|
|
33745
33813
|
}
|
|
33746
33814
|
}
|
|
33747
33815
|
}
|
|
@@ -33771,18 +33839,18 @@ class SiteGenerator {
|
|
|
33771
33839
|
noindex: page > 2
|
|
33772
33840
|
});
|
|
33773
33841
|
if (page === 1) {
|
|
33774
|
-
await Bun.write(
|
|
33842
|
+
await Bun.write(path6.join(this.options.outputDir, "index.html"), pageHtml);
|
|
33775
33843
|
} else {
|
|
33776
|
-
const pageDir =
|
|
33844
|
+
const pageDir = path6.join(this.options.outputDir, "page", page.toString());
|
|
33777
33845
|
await ensureDir(pageDir);
|
|
33778
|
-
await Bun.write(
|
|
33846
|
+
await Bun.write(path6.join(pageDir, "index.html"), pageHtml);
|
|
33779
33847
|
}
|
|
33780
33848
|
}
|
|
33781
33849
|
}
|
|
33782
33850
|
async generatePostPages() {
|
|
33783
33851
|
for (const post of this.site.posts) {
|
|
33784
33852
|
const postPath = post.url.substring(1);
|
|
33785
|
-
const postDir =
|
|
33853
|
+
const postDir = path6.join(this.options.outputDir, postPath);
|
|
33786
33854
|
await ensureDir(postDir);
|
|
33787
33855
|
const imageUrl = extractFirstImageUrl(post.html, this.options.config.baseUrl);
|
|
33788
33856
|
const schemas = generatePostPageSchemas({
|
|
@@ -33797,19 +33865,19 @@ class SiteGenerator {
|
|
|
33797
33865
|
post,
|
|
33798
33866
|
jsonLd
|
|
33799
33867
|
});
|
|
33800
|
-
await Bun.write(
|
|
33868
|
+
await Bun.write(path6.join(postDir, "index.html"), postHtml);
|
|
33801
33869
|
}
|
|
33802
33870
|
}
|
|
33803
33871
|
async generateTagPages() {
|
|
33804
|
-
const tagsDir =
|
|
33872
|
+
const tagsDir = path6.join(this.options.outputDir, "tags");
|
|
33805
33873
|
await ensureDir(tagsDir);
|
|
33806
33874
|
const tagIndexHtml = import_nunjucks.default.render("tags.njk", {
|
|
33807
33875
|
site: this.options.config,
|
|
33808
33876
|
tags: this.getSortedTags()
|
|
33809
33877
|
});
|
|
33810
|
-
await Bun.write(
|
|
33878
|
+
await Bun.write(path6.join(tagsDir, "index.html"), tagIndexHtml);
|
|
33811
33879
|
for (const [tagName, tagData] of Object.entries(this.site.tags)) {
|
|
33812
|
-
const tagDir =
|
|
33880
|
+
const tagDir = path6.join(tagsDir, tagData.slug);
|
|
33813
33881
|
await ensureDir(tagDir);
|
|
33814
33882
|
const pageSize = 10;
|
|
33815
33883
|
const totalPages = Math.ceil(tagData.posts.length / pageSize);
|
|
@@ -33852,24 +33920,24 @@ class SiteGenerator {
|
|
|
33852
33920
|
jsonLd
|
|
33853
33921
|
});
|
|
33854
33922
|
if (page === 1) {
|
|
33855
|
-
await Bun.write(
|
|
33923
|
+
await Bun.write(path6.join(tagDir, "index.html"), tagPageHtml);
|
|
33856
33924
|
} else {
|
|
33857
|
-
const pageDir =
|
|
33925
|
+
const pageDir = path6.join(tagDir, "page", page.toString());
|
|
33858
33926
|
await ensureDir(pageDir);
|
|
33859
|
-
await Bun.write(
|
|
33927
|
+
await Bun.write(path6.join(pageDir, "index.html"), tagPageHtml);
|
|
33860
33928
|
}
|
|
33861
33929
|
}
|
|
33862
33930
|
}
|
|
33863
33931
|
}
|
|
33864
33932
|
async generateMapPage() {
|
|
33865
33933
|
try {
|
|
33866
|
-
const mapDir =
|
|
33934
|
+
const mapDir = path6.join(this.options.outputDir, "map");
|
|
33867
33935
|
await ensureDir(mapDir);
|
|
33868
33936
|
const mapHtml = import_nunjucks.default.render("map.njk", {
|
|
33869
33937
|
site: this.options.config,
|
|
33870
33938
|
posts: this.site.posts
|
|
33871
33939
|
});
|
|
33872
|
-
await Bun.write(
|
|
33940
|
+
await Bun.write(path6.join(mapDir, "index.html"), mapHtml);
|
|
33873
33941
|
console.log("Generated map page");
|
|
33874
33942
|
} catch (error) {
|
|
33875
33943
|
if (error instanceof Error && error.message.includes("map.njk")) {
|
|
@@ -33899,7 +33967,7 @@ class SiteGenerator {
|
|
|
33899
33967
|
}
|
|
33900
33968
|
}
|
|
33901
33969
|
async fallbackCSSGeneration(cssConfig) {
|
|
33902
|
-
const cssFilePath =
|
|
33970
|
+
const cssFilePath = path6.resolve(process.cwd(), cssConfig.input);
|
|
33903
33971
|
const cssFile = Bun.file(cssFilePath);
|
|
33904
33972
|
if (!await cssFile.exists()) {
|
|
33905
33973
|
console.warn(`CSS input file not found: ${cssFilePath}`);
|
|
@@ -33907,8 +33975,8 @@ class SiteGenerator {
|
|
|
33907
33975
|
}
|
|
33908
33976
|
try {
|
|
33909
33977
|
const cssContent = await cssFile.text();
|
|
33910
|
-
const outputPath =
|
|
33911
|
-
const outputDir =
|
|
33978
|
+
const outputPath = path6.resolve(this.options.outputDir, cssConfig.output);
|
|
33979
|
+
const outputDir = path6.dirname(outputPath);
|
|
33912
33980
|
await ensureDir(outputDir);
|
|
33913
33981
|
await Bun.write(outputPath, cssContent);
|
|
33914
33982
|
console.log("\u2705 CSS file copied successfully (fallback mode)");
|
|
@@ -33917,8 +33985,8 @@ class SiteGenerator {
|
|
|
33917
33985
|
}
|
|
33918
33986
|
}
|
|
33919
33987
|
async copyStaticAssets() {
|
|
33920
|
-
const assetsDir =
|
|
33921
|
-
const publicDir =
|
|
33988
|
+
const assetsDir = path6.join(this.options.templatesDir, "assets");
|
|
33989
|
+
const publicDir = path6.join(process.cwd(), "public");
|
|
33922
33990
|
async function dirExists(p) {
|
|
33923
33991
|
try {
|
|
33924
33992
|
const stat = await fs2.promises.stat(p);
|
|
@@ -33930,15 +33998,15 @@ class SiteGenerator {
|
|
|
33930
33998
|
const assetsDirFile = Bun.file(assetsDir);
|
|
33931
33999
|
if (await assetsDirFile.exists() && await dirExists(assetsDir)) {
|
|
33932
34000
|
const assetGlob = new Glob2("**/*.*");
|
|
33933
|
-
const assetsOutputDir =
|
|
34001
|
+
const assetsOutputDir = path6.join(this.options.outputDir, "assets");
|
|
33934
34002
|
await ensureDir(assetsOutputDir);
|
|
33935
34003
|
for await (const file of assetGlob.scan({
|
|
33936
34004
|
cwd: assetsDir,
|
|
33937
34005
|
absolute: true
|
|
33938
34006
|
})) {
|
|
33939
|
-
const relativePath =
|
|
33940
|
-
const targetPath =
|
|
33941
|
-
const targetDir =
|
|
34007
|
+
const relativePath = path6.relative(assetsDir, file);
|
|
34008
|
+
const targetPath = path6.join(assetsOutputDir, relativePath);
|
|
34009
|
+
const targetDir = path6.dirname(targetPath);
|
|
33942
34010
|
await ensureDir(targetDir);
|
|
33943
34011
|
await copyFile(file, targetPath);
|
|
33944
34012
|
}
|
|
@@ -33949,9 +34017,9 @@ class SiteGenerator {
|
|
|
33949
34017
|
withFileTypes: true
|
|
33950
34018
|
});
|
|
33951
34019
|
for (const entry of entries) {
|
|
33952
|
-
const srcPath =
|
|
33953
|
-
const relativePath =
|
|
33954
|
-
const destPath =
|
|
34020
|
+
const srcPath = path6.join(srcDir, entry.name);
|
|
34021
|
+
const relativePath = path6.relative(publicDir, srcPath);
|
|
34022
|
+
const destPath = path6.join(this.options.outputDir, relativePath);
|
|
33955
34023
|
if (!relativePath)
|
|
33956
34024
|
continue;
|
|
33957
34025
|
if (entry.isDirectory()) {
|
|
@@ -33960,7 +34028,7 @@ class SiteGenerator {
|
|
|
33960
34028
|
} else if (entry.isFile()) {
|
|
33961
34029
|
const targetFile = Bun.file(destPath);
|
|
33962
34030
|
if (!await targetFile.exists()) {
|
|
33963
|
-
const targetDir =
|
|
34031
|
+
const targetDir = path6.dirname(destPath);
|
|
33964
34032
|
await ensureDir(targetDir);
|
|
33965
34033
|
await copyFile(srcPath, destPath);
|
|
33966
34034
|
}
|
|
@@ -34060,7 +34128,7 @@ ${channelXml}
|
|
|
34060
34128
|
${rssItems}
|
|
34061
34129
|
</channel>
|
|
34062
34130
|
</rss>`;
|
|
34063
|
-
await Bun.write(
|
|
34131
|
+
await Bun.write(path6.join(this.options.outputDir, "feed.xml"), rssContent);
|
|
34064
34132
|
}
|
|
34065
34133
|
async generateSitemap() {
|
|
34066
34134
|
const currentDate = toPacificTime(new Date).toISOString();
|
|
@@ -34179,7 +34247,7 @@ ${rssItems}
|
|
|
34179
34247
|
}
|
|
34180
34248
|
}
|
|
34181
34249
|
sitemapContent += `</urlset>`;
|
|
34182
|
-
await Bun.write(
|
|
34250
|
+
await Bun.write(path6.join(this.options.outputDir, "sitemap.xml"), sitemapContent);
|
|
34183
34251
|
console.log("Generated sitemap.xml");
|
|
34184
34252
|
const urlCount = this.site.posts.length + Object.keys(this.site.tags).length + 10;
|
|
34185
34253
|
const sitemapSize = sitemapContent.length;
|
|
@@ -34199,7 +34267,7 @@ ${rssItems}
|
|
|
34199
34267
|
</sitemap>
|
|
34200
34268
|
`;
|
|
34201
34269
|
sitemapIndexContent += `</sitemapindex>`;
|
|
34202
|
-
await Bun.write(
|
|
34270
|
+
await Bun.write(path6.join(this.options.outputDir, "sitemap_index.xml"), sitemapIndexContent);
|
|
34203
34271
|
console.log("Generated sitemap_index.xml");
|
|
34204
34272
|
}
|
|
34205
34273
|
async generateRobotsTxt() {
|
|
@@ -34221,7 +34289,7 @@ Sitemap: ${config.baseUrl}/sitemap.xml
|
|
|
34221
34289
|
# Disallow: /admin/
|
|
34222
34290
|
# Disallow: /api/
|
|
34223
34291
|
`;
|
|
34224
|
-
await Bun.write(
|
|
34292
|
+
await Bun.write(path6.join(this.options.outputDir, "robots.txt"), robotsTxtContent);
|
|
34225
34293
|
console.log("Generated robots.txt");
|
|
34226
34294
|
}
|
|
34227
34295
|
}
|
|
@@ -34235,10 +34303,10 @@ var defaultDeps2 = {
|
|
|
34235
34303
|
};
|
|
34236
34304
|
async function handleGenerateCommand(options2, deps = defaultDeps2) {
|
|
34237
34305
|
try {
|
|
34238
|
-
const configPath =
|
|
34239
|
-
const contentDir =
|
|
34240
|
-
const outputDir =
|
|
34241
|
-
const templatesDir =
|
|
34306
|
+
const configPath = path7.resolve(options2.config);
|
|
34307
|
+
const contentDir = path7.resolve(options2.content);
|
|
34308
|
+
const outputDir = path7.resolve(options2.output);
|
|
34309
|
+
const templatesDir = path7.resolve(options2.templates);
|
|
34242
34310
|
deps.logger.log("Generating site with:");
|
|
34243
34311
|
deps.logger.log(`- Config file: ${configPath}`);
|
|
34244
34312
|
deps.logger.log(`- Content directory: ${contentDir}`);
|
|
@@ -34266,11 +34334,11 @@ function registerGenerateCommand(program2) {
|
|
|
34266
34334
|
}
|
|
34267
34335
|
|
|
34268
34336
|
// src/utils/image-uploader.ts
|
|
34269
|
-
import
|
|
34337
|
+
import path9 from "path";
|
|
34270
34338
|
|
|
34271
34339
|
// src/utils/s3-uploader.ts
|
|
34272
34340
|
var {S3Client } = globalThis.Bun;
|
|
34273
|
-
import
|
|
34341
|
+
import path8 from "path";
|
|
34274
34342
|
|
|
34275
34343
|
class S3Uploader {
|
|
34276
34344
|
s3Config;
|
|
@@ -34393,8 +34461,8 @@ class S3Uploader {
|
|
|
34393
34461
|
let failedCount = 0;
|
|
34394
34462
|
const uploadTasks = imageFiles.map((imageFile) => async () => {
|
|
34395
34463
|
try {
|
|
34396
|
-
const imagePath =
|
|
34397
|
-
const filename =
|
|
34464
|
+
const imagePath = path8.join(imagesDir, imageFile);
|
|
34465
|
+
const filename = path8.basename(imagePath);
|
|
34398
34466
|
const file = Bun.file(imagePath);
|
|
34399
34467
|
const contentType = file.type;
|
|
34400
34468
|
if (process.env.BUNKI_DRY_RUN === "true") {} else {
|
|
@@ -34428,10 +34496,10 @@ function createUploader(config) {
|
|
|
34428
34496
|
}
|
|
34429
34497
|
|
|
34430
34498
|
// src/utils/image-uploader.ts
|
|
34431
|
-
var DEFAULT_IMAGES_DIR =
|
|
34499
|
+
var DEFAULT_IMAGES_DIR = path9.join(process.cwd(), "assets");
|
|
34432
34500
|
async function uploadImages(options2 = {}) {
|
|
34433
34501
|
try {
|
|
34434
|
-
const imagesDir =
|
|
34502
|
+
const imagesDir = path9.resolve(options2.images || DEFAULT_IMAGES_DIR);
|
|
34435
34503
|
if (!await fileExists(imagesDir)) {
|
|
34436
34504
|
console.log(`Creating images directory at ${imagesDir}...`);
|
|
34437
34505
|
await ensureDir(imagesDir);
|
|
@@ -34468,7 +34536,7 @@ async function uploadImages(options2 = {}) {
|
|
|
34468
34536
|
const uploader = createUploader(s3Config);
|
|
34469
34537
|
const imageUrlMap = await uploader.uploadImages(imagesDir, options2.minYear);
|
|
34470
34538
|
if (options2.outputJson) {
|
|
34471
|
-
const outputFile =
|
|
34539
|
+
const outputFile = path9.resolve(options2.outputJson);
|
|
34472
34540
|
await Bun.write(outputFile, JSON.stringify(imageUrlMap, null, 2));
|
|
34473
34541
|
console.log(`Image URL mapping saved to ${outputFile}`);
|
|
34474
34542
|
}
|
|
@@ -34514,7 +34582,7 @@ function registerImagesPushCommand(program2) {
|
|
|
34514
34582
|
}
|
|
34515
34583
|
|
|
34516
34584
|
// src/cli/commands/init.ts
|
|
34517
|
-
import
|
|
34585
|
+
import path10 from "path";
|
|
34518
34586
|
var defaultDependencies = {
|
|
34519
34587
|
createDefaultConfig,
|
|
34520
34588
|
ensureDir,
|
|
@@ -34524,7 +34592,7 @@ var defaultDependencies = {
|
|
|
34524
34592
|
};
|
|
34525
34593
|
async function handleInitCommand(options2, deps = defaultDependencies) {
|
|
34526
34594
|
try {
|
|
34527
|
-
const configPath =
|
|
34595
|
+
const configPath = path10.resolve(options2.config);
|
|
34528
34596
|
const configCreated = await deps.createDefaultConfig(configPath);
|
|
34529
34597
|
if (!configCreated) {
|
|
34530
34598
|
deps.logger.log(`
|
|
@@ -34533,19 +34601,19 @@ Skipped initialization because the config file already exists`);
|
|
|
34533
34601
|
}
|
|
34534
34602
|
deps.logger.log("Creating directory structure...");
|
|
34535
34603
|
const baseDir = process.cwd();
|
|
34536
|
-
const contentDir =
|
|
34537
|
-
const templatesDir =
|
|
34538
|
-
const stylesDir =
|
|
34539
|
-
const publicDir =
|
|
34604
|
+
const contentDir = path10.join(baseDir, "content");
|
|
34605
|
+
const templatesDir = path10.join(baseDir, "templates");
|
|
34606
|
+
const stylesDir = path10.join(templatesDir, "styles");
|
|
34607
|
+
const publicDir = path10.join(baseDir, "public");
|
|
34540
34608
|
await deps.ensureDir(contentDir);
|
|
34541
34609
|
await deps.ensureDir(templatesDir);
|
|
34542
34610
|
await deps.ensureDir(stylesDir);
|
|
34543
34611
|
await deps.ensureDir(publicDir);
|
|
34544
34612
|
for (const [filename, content] of Object.entries(getDefaultTemplates())) {
|
|
34545
|
-
await deps.writeFile(
|
|
34613
|
+
await deps.writeFile(path10.join(templatesDir, filename), content);
|
|
34546
34614
|
}
|
|
34547
|
-
await deps.writeFile(
|
|
34548
|
-
await deps.writeFile(
|
|
34615
|
+
await deps.writeFile(path10.join(stylesDir, "main.css"), getDefaultCss());
|
|
34616
|
+
await deps.writeFile(path10.join(contentDir, "welcome.md"), getSamplePost());
|
|
34549
34617
|
deps.logger.log(`
|
|
34550
34618
|
Initialization complete! Here are the next steps:`);
|
|
34551
34619
|
deps.logger.log("1. Edit bunki.config.ts to configure your site");
|
|
@@ -35037,7 +35105,7 @@ function hello() {
|
|
|
35037
35105
|
}
|
|
35038
35106
|
|
|
35039
35107
|
// src/cli/commands/new-post.ts
|
|
35040
|
-
import
|
|
35108
|
+
import path11 from "path";
|
|
35041
35109
|
var defaultDeps4 = {
|
|
35042
35110
|
writeFile: (filePath, data) => Bun.write(filePath, data),
|
|
35043
35111
|
now: () => new Date,
|
|
@@ -35061,7 +35129,7 @@ async function handleNewCommand(title, options2, deps = defaultDeps4) {
|
|
|
35061
35129
|
` + `# ${title}
|
|
35062
35130
|
|
|
35063
35131
|
`;
|
|
35064
|
-
const filePath =
|
|
35132
|
+
const filePath = path11.join(DEFAULT_CONTENT_DIR, `${slug}.md`);
|
|
35065
35133
|
await deps.writeFile(filePath, frontmatter);
|
|
35066
35134
|
deps.logger.log(`Created new post: ${filePath}`);
|
|
35067
35135
|
return filePath;
|
|
@@ -35078,11 +35146,11 @@ function registerNewCommand(program2) {
|
|
|
35078
35146
|
}
|
|
35079
35147
|
|
|
35080
35148
|
// src/cli/commands/serve.ts
|
|
35081
|
-
import
|
|
35149
|
+
import path13 from "path";
|
|
35082
35150
|
|
|
35083
35151
|
// src/server.ts
|
|
35084
35152
|
import fs3 from "fs";
|
|
35085
|
-
import
|
|
35153
|
+
import path12 from "path";
|
|
35086
35154
|
async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
35087
35155
|
try {
|
|
35088
35156
|
const stats = await fs3.promises.stat(outputDir);
|
|
@@ -35117,18 +35185,18 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35117
35185
|
let filePath = "";
|
|
35118
35186
|
if (homePaginationMatch) {
|
|
35119
35187
|
const pageNumber = homePaginationMatch[1];
|
|
35120
|
-
filePath =
|
|
35188
|
+
filePath = path12.join(outputDir, "page", pageNumber, "index.html");
|
|
35121
35189
|
} else if (tagPaginationMatch) {
|
|
35122
35190
|
const tagSlug = tagPaginationMatch[1];
|
|
35123
35191
|
const pageNumber = tagPaginationMatch[2];
|
|
35124
|
-
filePath =
|
|
35192
|
+
filePath = path12.join(outputDir, "tags", tagSlug, "page", pageNumber, "index.html");
|
|
35125
35193
|
} else if (yearPaginationMatch) {
|
|
35126
35194
|
const year = yearPaginationMatch[1];
|
|
35127
35195
|
const pageNumber = yearPaginationMatch[2];
|
|
35128
|
-
filePath =
|
|
35196
|
+
filePath = path12.join(outputDir, year, "page", pageNumber, "index.html");
|
|
35129
35197
|
} else {
|
|
35130
|
-
const directPath =
|
|
35131
|
-
const withoutSlash =
|
|
35198
|
+
const directPath = path12.join(outputDir, pathname);
|
|
35199
|
+
const withoutSlash = path12.join(outputDir, pathname + ".html");
|
|
35132
35200
|
const withHtml = pathname.endsWith(".html") ? directPath : withoutSlash;
|
|
35133
35201
|
const bunFileDirect = Bun.file(directPath);
|
|
35134
35202
|
const bunFileHtml = Bun.file(withHtml);
|
|
@@ -35137,7 +35205,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35137
35205
|
} else if (await bunFileHtml.exists()) {
|
|
35138
35206
|
filePath = withHtml;
|
|
35139
35207
|
} else {
|
|
35140
|
-
const indexPath =
|
|
35208
|
+
const indexPath = path12.join(outputDir, pathname, "index.html");
|
|
35141
35209
|
const bunFileIndex = Bun.file(indexPath);
|
|
35142
35210
|
if (await bunFileIndex.exists()) {
|
|
35143
35211
|
filePath = indexPath;
|
|
@@ -35151,7 +35219,7 @@ async function startServer(outputDir = DEFAULT_OUTPUT_DIR, port = 3000) {
|
|
|
35151
35219
|
}
|
|
35152
35220
|
}
|
|
35153
35221
|
console.log(`Serving file: ${filePath}`);
|
|
35154
|
-
const extname =
|
|
35222
|
+
const extname = path12.extname(filePath);
|
|
35155
35223
|
let contentType = "text/html";
|
|
35156
35224
|
switch (extname) {
|
|
35157
35225
|
case ".js":
|
|
@@ -35210,7 +35278,7 @@ var defaultDeps5 = {
|
|
|
35210
35278
|
};
|
|
35211
35279
|
async function handleServeCommand(options2, deps = defaultDeps5) {
|
|
35212
35280
|
try {
|
|
35213
|
-
const outputDir =
|
|
35281
|
+
const outputDir = path13.resolve(options2.output);
|
|
35214
35282
|
const port = parseInt(options2.port, 10);
|
|
35215
35283
|
await deps.startServer(outputDir, port);
|
|
35216
35284
|
} catch (error) {
|
|
@@ -35251,6 +35319,12 @@ function registerValidateCommand(program2) {
|
|
|
35251
35319
|
}
|
|
35252
35320
|
|
|
35253
35321
|
// src/cli.ts
|
|
35322
|
+
import { readFileSync } from "fs";
|
|
35323
|
+
import { join, dirname } from "path";
|
|
35324
|
+
import { fileURLToPath } from "url";
|
|
35325
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
35326
|
+
var packageJson = JSON.parse(readFileSync(join(__dirname2, "../package.json"), "utf-8"));
|
|
35327
|
+
var VERSION = packageJson.version;
|
|
35254
35328
|
var program2 = new Command;
|
|
35255
35329
|
registerInitCommand(program2);
|
|
35256
35330
|
registerNewCommand(program2);
|
|
@@ -35259,7 +35333,7 @@ registerServeCommand(program2);
|
|
|
35259
35333
|
registerCssCommand(program2);
|
|
35260
35334
|
registerImagesPushCommand(program2);
|
|
35261
35335
|
registerValidateCommand(program2);
|
|
35262
|
-
program2.name("bunki").description("An opinionated static site generator built with Bun").version(
|
|
35336
|
+
program2.name("bunki").description("An opinionated static site generator built with Bun").version(VERSION);
|
|
35263
35337
|
var currentFile = import.meta.url.replace("file://", "");
|
|
35264
35338
|
var mainFile = Bun.main;
|
|
35265
35339
|
if (currentFile === mainFile || currentFile.endsWith(mainFile)) {
|