@daz4126/swifty 2.10.0 → 2.12.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 +16 -3
- package/package.json +3 -2
- package/src/assets.js +106 -54
- package/src/build.js +57 -1
- package/src/cli.js +51 -7
- package/src/concurrency.js +25 -0
- package/src/config.js +22 -2
- package/src/data.js +70 -0
- package/src/init.js +36 -33
- package/src/layout.js +3 -3
- package/src/pages.js +236 -118
- package/src/pagination.js +58 -0
- package/src/partials.js +236 -42
- package/src/rss.js +6 -4
- package/src/sitemap.js +89 -0
- package/src/watcher.js +40 -12
package/README.md
CHANGED
|
@@ -13,13 +13,23 @@ Swifty uses convention over configuration to make it super simple to build blazi
|
|
|
13
13
|
- **Code syntax highlighting** via highlight.js
|
|
14
14
|
- **Tags and navigation** generated automatically
|
|
15
15
|
- **RSS feed generation** for blogs and content folders
|
|
16
|
+
- **Draft mode** for work-in-progress pages (visible in dev, hidden in production)
|
|
17
|
+
- **Scheduled publishing** via future dates in front matter
|
|
18
|
+
- **Contact forms** via third-party services (Formspree, Netlify Forms, etc.)
|
|
19
|
+
- **Pagination** for folders with many pages
|
|
20
|
+
- **Data files** - Load JSON/YAML data and use in templates
|
|
21
|
+
- **Open Graph tags** - Auto-generated social sharing meta tags
|
|
22
|
+
- **Word count & reading time** - Auto-calculated for blog posts
|
|
23
|
+
- **Previous/next navigation** - Auto-generated links between sibling pages
|
|
24
|
+
- **[Eta templating](https://eta.js.org/)** - Full JavaScript in templates with EJS syntax
|
|
16
25
|
- **Optional [Turbo](https://turbo.hotwired.dev/)** for SPA-like transitions
|
|
17
26
|
|
|
18
27
|
## Quickstart
|
|
19
28
|
|
|
20
29
|
```bash
|
|
21
|
-
npm install @daz4126/swifty
|
|
22
|
-
|
|
30
|
+
npm install -g @daz4126/swifty
|
|
31
|
+
swifty my-site
|
|
32
|
+
cd my-site
|
|
23
33
|
npx swifty start
|
|
24
34
|
```
|
|
25
35
|
|
|
@@ -32,6 +42,7 @@ your-site/
|
|
|
32
42
|
├── pages/ # Markdown content (folder structure = URLs)
|
|
33
43
|
├── layouts/ # HTML layout templates
|
|
34
44
|
├── partials/ # Reusable content snippets
|
|
45
|
+
├── data/ # JSON/YAML data files
|
|
35
46
|
├── css/ # Stylesheets (auto-injected)
|
|
36
47
|
├── js/ # JavaScript (auto-injected)
|
|
37
48
|
├── images/ # Images (auto-optimized to WebP)
|
|
@@ -42,16 +53,18 @@ your-site/
|
|
|
42
53
|
## Commands
|
|
43
54
|
|
|
44
55
|
```bash
|
|
45
|
-
npx swifty
|
|
56
|
+
npx swifty my-site # Create new site in my-site/ folder
|
|
46
57
|
npx swifty build # Build static site to dist/ (for production)
|
|
47
58
|
npx swifty start # Build, watch, and serve at localhost:3000 (for development)
|
|
48
59
|
npx swifty build --out dir # Build to custom output directory
|
|
60
|
+
npx swifty deploy "message" # Build, git add, commit, and push (message optional)
|
|
49
61
|
```
|
|
50
62
|
|
|
51
63
|
### Development vs Production
|
|
52
64
|
|
|
53
65
|
- **`swifty start`** - For development. Includes live reload (auto-refreshes browser on file changes) and file watching with incremental builds for CSS/JS/images.
|
|
54
66
|
- **`swifty build`** - For production deployment. Produces clean output without any development scripts.
|
|
67
|
+
- **`swifty deploy "message"`** - Builds the site and commits/pushes to git in one command.
|
|
55
68
|
|
|
56
69
|
## Documentation
|
|
57
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/swifty",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"description": "Super Speedy Static Site Generator",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"chokidar": "^4.0.0",
|
|
29
|
+
"eta": "^4.5.0",
|
|
29
30
|
"fs-extra": "^11.2.0",
|
|
30
31
|
"gray-matter": "^4.0.3",
|
|
31
32
|
"highlight.js": "^11.11.1",
|
|
@@ -33,7 +34,7 @@
|
|
|
33
34
|
"livereload": "^0.10.3",
|
|
34
35
|
"marked": "^14.1.3",
|
|
35
36
|
"marked-highlight": "^2.2.1",
|
|
36
|
-
"serve": "^14.2.
|
|
37
|
+
"serve": "^14.2.5",
|
|
37
38
|
"sharp": "^0.33.5"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
package/src/assets.js
CHANGED
|
@@ -3,11 +3,64 @@ import fsExtra from "fs-extra";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import sharp from "sharp";
|
|
5
5
|
import { dirs, defaultConfig } from "./config.js";
|
|
6
|
+
import { mapLimit } from "./concurrency.js";
|
|
7
|
+
|
|
8
|
+
// Get file modification timestamp for cache busting
|
|
9
|
+
const getFileMtime = async (filePath) => {
|
|
10
|
+
const stats = await fs.stat(filePath);
|
|
11
|
+
return Math.floor(stats.mtimeMs);
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
const validExtensions = {
|
|
8
15
|
css: [".css"],
|
|
9
16
|
js: [".js"],
|
|
10
|
-
images: [".png", ".jpg", ".jpeg", ".gif", ".svg", "
|
|
17
|
+
images: [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const optimizableImageExtensions = [".jpg", ".jpeg", ".png"];
|
|
21
|
+
|
|
22
|
+
const getBuildConcurrency = () => defaultConfig.build_concurrency || 16;
|
|
23
|
+
|
|
24
|
+
const isDestinationFresh = async (source, destination) => {
|
|
25
|
+
try {
|
|
26
|
+
const [sourceStats, destinationStats] = await Promise.all([
|
|
27
|
+
fs.stat(source),
|
|
28
|
+
fs.stat(destination),
|
|
29
|
+
]);
|
|
30
|
+
return destinationStats.mtimeMs >= sourceStats.mtimeMs;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const copyIfStale = async (source, destination) => {
|
|
37
|
+
if (await isDestinationFresh(source, destination)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await fsExtra.copy(source, destination);
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const optimizeImageToWebp = async (source, destination) => {
|
|
46
|
+
if (await isDestinationFresh(source, destination)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const image = sharp(source);
|
|
51
|
+
const metadata = await image.metadata();
|
|
52
|
+
const originalWidth = metadata.width || 0;
|
|
53
|
+
const maxWidth = defaultConfig.max_image_width || 800;
|
|
54
|
+
const imageQuality = defaultConfig.image_quality || 80;
|
|
55
|
+
const resizeOptions =
|
|
56
|
+
originalWidth > 0 ? { width: Math.min(originalWidth, maxWidth) } : {};
|
|
57
|
+
|
|
58
|
+
await image
|
|
59
|
+
.resize(resizeOptions)
|
|
60
|
+
.toFormat("webp", { quality: imageQuality })
|
|
61
|
+
.toFile(destination);
|
|
62
|
+
|
|
63
|
+
return true;
|
|
11
64
|
};
|
|
12
65
|
|
|
13
66
|
const ensureAndCopy = async (source, destination, validExts) => {
|
|
@@ -34,47 +87,47 @@ const copyAssets = async (outputDir = dirs.dist) => {
|
|
|
34
87
|
validExtensions.css,
|
|
35
88
|
);
|
|
36
89
|
await ensureAndCopy(dirs.js, path.join(outputDir, "js"), validExtensions.js);
|
|
37
|
-
await ensureAndCopy(
|
|
38
|
-
dirs.images,
|
|
39
|
-
path.join(outputDir, "images"),
|
|
40
|
-
validExtensions.images,
|
|
41
|
-
);
|
|
42
90
|
};
|
|
43
91
|
async function optimizeImages(outputDir = dirs.dist) {
|
|
44
92
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
93
|
+
if (!(await fsExtra.pathExists(dirs.images))) {
|
|
94
|
+
console.log(`No ${path.basename(dirs.images)} found in ${dirs.images}`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const imagesFolder = path.join(outputDir, "images");
|
|
99
|
+
await fsExtra.ensureDir(imagesFolder);
|
|
100
|
+
|
|
101
|
+
const files = await fs.readdir(dirs.images);
|
|
102
|
+
|
|
103
|
+
await mapLimit(
|
|
104
|
+
files.filter((file) =>
|
|
105
|
+
validExtensions.images.includes(path.extname(file).toLowerCase()),
|
|
106
|
+
),
|
|
107
|
+
async (file) => {
|
|
108
|
+
const filePath = path.join(dirs.images, file);
|
|
52
109
|
const ext = path.extname(file).toLowerCase();
|
|
53
110
|
|
|
54
|
-
if (!
|
|
111
|
+
if (!optimizableImageExtensions.includes(ext)) {
|
|
112
|
+
const destination = path.join(imagesFolder, file);
|
|
113
|
+
const copied = await copyIfStale(filePath, destination);
|
|
114
|
+
if (copied) {
|
|
115
|
+
console.log(`Copied ${file}`);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
55
119
|
|
|
56
120
|
const optimizedPath = path.join(
|
|
57
|
-
|
|
121
|
+
imagesFolder,
|
|
58
122
|
`${path.basename(file, ext)}.webp`,
|
|
59
123
|
);
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const metadata = await image.metadata();
|
|
64
|
-
const originalWidth = metadata.width || 0;
|
|
65
|
-
const maxWidth = defaultConfig.max_image_size || 800;
|
|
66
|
-
const resizeWidth = Math.min(originalWidth, maxWidth);
|
|
67
|
-
|
|
68
|
-
await image
|
|
69
|
-
.resize({ width: resizeWidth })
|
|
70
|
-
.toFormat("webp", { quality: 80 })
|
|
71
|
-
.toFile(optimizedPath);
|
|
72
|
-
|
|
73
|
-
await fs.unlink(filePath);
|
|
74
|
-
|
|
125
|
+
const optimized = await optimizeImageToWebp(filePath, optimizedPath);
|
|
126
|
+
if (optimized) {
|
|
75
127
|
console.log(`Optimized ${file} -> ${optimizedPath}`);
|
|
76
128
|
}
|
|
77
|
-
}
|
|
129
|
+
},
|
|
130
|
+
getBuildConcurrency(),
|
|
78
131
|
);
|
|
79
132
|
} catch (error) {
|
|
80
133
|
console.error("Error optimizing images:", error);
|
|
@@ -83,34 +136,40 @@ async function optimizeImages(outputDir = dirs.dist) {
|
|
|
83
136
|
const generateAssetImports = async (dir, tagTemplate, validExts) => {
|
|
84
137
|
if (!(await fsExtra.pathExists(dir))) return "";
|
|
85
138
|
const files = await fs.readdir(dir);
|
|
86
|
-
|
|
139
|
+
const validFiles = files
|
|
87
140
|
.filter((file) => validExts.includes(path.extname(file).toLowerCase()))
|
|
88
|
-
.sort()
|
|
89
|
-
|
|
90
|
-
|
|
141
|
+
.sort();
|
|
142
|
+
|
|
143
|
+
const imports = await Promise.all(
|
|
144
|
+
validFiles.map(async (file) => {
|
|
145
|
+
const mtime = await getFileMtime(path.join(dir, file));
|
|
146
|
+
return tagTemplate(file, mtime);
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
return imports.join("\n");
|
|
91
150
|
};
|
|
92
151
|
const getCssImports = () =>
|
|
93
152
|
generateAssetImports(
|
|
94
153
|
dirs.css,
|
|
95
|
-
(file) => `<link rel="stylesheet" href="/css/${file}" />`,
|
|
154
|
+
(file, mtime) => `<link rel="stylesheet" href="/css/${file}?v=${mtime}" />`,
|
|
96
155
|
validExtensions.css,
|
|
97
156
|
);
|
|
98
157
|
const getJsImports = () =>
|
|
99
158
|
generateAssetImports(
|
|
100
159
|
dirs.js,
|
|
101
|
-
(file) => `<script src="/js/${file}"></script>`,
|
|
160
|
+
(file, mtime) => `<script src="/js/${file}?v=${mtime}"></script>`,
|
|
102
161
|
validExtensions.js,
|
|
103
162
|
);
|
|
104
163
|
const getCssPreloads = () =>
|
|
105
164
|
generateAssetImports(
|
|
106
165
|
dirs.css,
|
|
107
|
-
(file) => `<link rel="preload" href="/css/${file}" as="style" />`,
|
|
166
|
+
(file, mtime) => `<link rel="preload" href="/css/${file}?v=${mtime}" as="style" />`,
|
|
108
167
|
validExtensions.css,
|
|
109
168
|
);
|
|
110
169
|
const getJsPreloads = () =>
|
|
111
170
|
generateAssetImports(
|
|
112
171
|
dirs.js,
|
|
113
|
-
(file) => `<link rel="preload" href="/js/${file}" as="script" />`,
|
|
172
|
+
(file, mtime) => `<link rel="preload" href="/js/${file}?v=${mtime}" as="script" />`,
|
|
114
173
|
validExtensions.js,
|
|
115
174
|
);
|
|
116
175
|
|
|
@@ -136,7 +195,6 @@ const copySingleAsset = async (filePath, outputDir = dirs.dist) => {
|
|
|
136
195
|
|
|
137
196
|
// Process a single image
|
|
138
197
|
const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
|
|
139
|
-
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
|
|
140
198
|
const filename = path.basename(filePath);
|
|
141
199
|
const ext = path.extname(filePath).toLowerCase();
|
|
142
200
|
const imagesFolder = path.join(outputDir, "images");
|
|
@@ -144,26 +202,20 @@ const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
|
|
|
144
202
|
await fsExtra.ensureDir(imagesFolder);
|
|
145
203
|
|
|
146
204
|
// For non-optimizable images (svg, webp, gif), just copy
|
|
147
|
-
if (!
|
|
148
|
-
await
|
|
149
|
-
|
|
205
|
+
if (!optimizableImageExtensions.includes(ext)) {
|
|
206
|
+
const copied = await copyIfStale(filePath, path.join(imagesFolder, filename));
|
|
207
|
+
if (copied) {
|
|
208
|
+
console.log(`Copied ${filename}`);
|
|
209
|
+
}
|
|
150
210
|
return true;
|
|
151
211
|
}
|
|
152
212
|
|
|
153
213
|
// Optimize jpg/jpeg/png to webp
|
|
154
214
|
const optimizedPath = path.join(imagesFolder, `${path.basename(filename, ext)}.webp`);
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const resizeWidth = Math.min(originalWidth, maxWidth);
|
|
160
|
-
|
|
161
|
-
await image
|
|
162
|
-
.resize({ width: resizeWidth })
|
|
163
|
-
.toFormat("webp", { quality: 80 })
|
|
164
|
-
.toFile(optimizedPath);
|
|
165
|
-
|
|
166
|
-
console.log(`Optimized ${filename} -> ${path.basename(optimizedPath)}`);
|
|
215
|
+
const optimized = await optimizeImageToWebp(filePath, optimizedPath);
|
|
216
|
+
if (optimized) {
|
|
217
|
+
console.log(`Optimized ${filename} -> ${path.basename(optimizedPath)}`);
|
|
218
|
+
}
|
|
167
219
|
return true;
|
|
168
220
|
};
|
|
169
221
|
|
package/src/build.js
CHANGED
|
@@ -1,13 +1,69 @@
|
|
|
1
1
|
import { copyAssets, optimizeImages } from "./assets.js";
|
|
2
2
|
import { generatePages, createPages, addLinks } from "./pages.js";
|
|
3
3
|
import { generateRssFeeds } from "./rss.js";
|
|
4
|
+
import { generateSeoFiles } from "./sitemap.js";
|
|
4
5
|
import { dirs } from "./config.js";
|
|
5
6
|
|
|
6
|
-
export default async function build(outputDir) {
|
|
7
|
+
export default async function build(outputDir = dirs.dist) {
|
|
8
|
+
const startTime = performance.now();
|
|
9
|
+
console.log("🚀 Starting build...");
|
|
10
|
+
|
|
11
|
+
const copyStart = performance.now();
|
|
7
12
|
await copyAssets(outputDir);
|
|
13
|
+
const copyTime = performance.now() - copyStart;
|
|
14
|
+
console.log(`📁 Assets copied in ${(copyTime / 1000).toFixed(2)}s`);
|
|
15
|
+
|
|
16
|
+
const imageStart = performance.now();
|
|
8
17
|
await optimizeImages(outputDir);
|
|
18
|
+
const imageTime = performance.now() - imageStart;
|
|
19
|
+
console.log(`🖼️ Images optimized in ${(imageTime / 1000).toFixed(2)}s`);
|
|
20
|
+
|
|
21
|
+
const pagesStart = performance.now();
|
|
9
22
|
const pages = await generatePages(dirs.pages);
|
|
23
|
+
const pagesTime = performance.now() - pagesStart;
|
|
24
|
+
console.log(`📄 Pages generated in ${(pagesTime / 1000).toFixed(2)}s`);
|
|
25
|
+
|
|
26
|
+
const linksStart = performance.now();
|
|
10
27
|
await addLinks(pages);
|
|
28
|
+
const linksTime = performance.now() - linksStart;
|
|
29
|
+
console.log(`🔗 Links added in ${(linksTime / 1000).toFixed(2)}s`);
|
|
30
|
+
|
|
31
|
+
const createStart = performance.now();
|
|
11
32
|
await createPages(pages, outputDir);
|
|
33
|
+
const createTime = performance.now() - createStart;
|
|
34
|
+
console.log(`✨ Pages created in ${(createTime / 1000).toFixed(2)}s`);
|
|
35
|
+
|
|
36
|
+
const rssStart = performance.now();
|
|
12
37
|
await generateRssFeeds(pages, outputDir);
|
|
38
|
+
const rssTime = performance.now() - rssStart;
|
|
39
|
+
console.log(`📡 RSS feeds generated in ${(rssTime / 1000).toFixed(2)}s`);
|
|
40
|
+
|
|
41
|
+
const seoStart = performance.now();
|
|
42
|
+
await generateSeoFiles(pages, outputDir);
|
|
43
|
+
const seoTime = performance.now() - seoStart;
|
|
44
|
+
console.log(`🧭 SEO files generated in ${(seoTime / 1000).toFixed(2)}s`);
|
|
45
|
+
|
|
46
|
+
const totalTime = performance.now() - startTime;
|
|
47
|
+
console.log(`\n✅ Build completed in ${(totalTime / 1000).toFixed(2)}s`);
|
|
48
|
+
|
|
49
|
+
// Performance summary
|
|
50
|
+
const stages = [
|
|
51
|
+
{ name: "Assets", time: copyTime },
|
|
52
|
+
{ name: "Images", time: imageTime },
|
|
53
|
+
{ name: "Pages", time: pagesTime },
|
|
54
|
+
{ name: "Links", time: linksTime },
|
|
55
|
+
{ name: "Create", time: createTime },
|
|
56
|
+
{ name: "RSS", time: rssTime },
|
|
57
|
+
{ name: "SEO", time: seoTime }
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const slowest = stages.sort((a, b) => b.time - a.time)[0];
|
|
61
|
+
if (slowest.time > totalTime * 0.3) {
|
|
62
|
+
console.log(`⚠️ Slowest stage: ${slowest.name} (${(slowest.time / 1000).toFixed(2)}s)`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Force output to appear
|
|
66
|
+
if (process.stdout.isTTY) {
|
|
67
|
+
process.stdout.write('\n');
|
|
68
|
+
}
|
|
13
69
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from "child_process";
|
|
3
|
+
import { spawn, execSync } from "child_process";
|
|
4
4
|
|
|
5
5
|
const args = process.argv.slice(2);
|
|
6
6
|
let outDir = "dist"; // default
|
|
@@ -14,13 +14,22 @@ if (outIndex !== -1 && args[outIndex + 1]) {
|
|
|
14
14
|
// Pass outDir as an environment variable too (optional, still useful)
|
|
15
15
|
process.env.OUT_DIR = outDir;
|
|
16
16
|
|
|
17
|
+
const reservedCommands = ["build", "start", "watch", "deploy"];
|
|
18
|
+
|
|
17
19
|
async function main() {
|
|
18
20
|
const command = args[0];
|
|
19
21
|
|
|
22
|
+
// No arguments - show usage
|
|
23
|
+
if (!command) {
|
|
24
|
+
console.log(`Usage:`);
|
|
25
|
+
console.log(` swifty <sitename> Create a new site in <sitename> folder`);
|
|
26
|
+
console.log(` swifty build [--out folder] Build the site`);
|
|
27
|
+
console.log(` swifty start [--out folder] Build and serve with live reload`);
|
|
28
|
+
console.log(` swifty deploy ["message"] Build, commit, and push to git`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
switch (command) {
|
|
21
|
-
case "init":
|
|
22
|
-
await import("./init.js");
|
|
23
|
-
break;
|
|
24
33
|
case "build": {
|
|
25
34
|
const build = await import("./build.js");
|
|
26
35
|
if (typeof build.default === "function") {
|
|
@@ -54,9 +63,44 @@ async function main() {
|
|
|
54
63
|
}
|
|
55
64
|
break;
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
case "deploy": {
|
|
67
|
+
const commitMessage = args[1] || "Deploying latest build";
|
|
68
|
+
|
|
69
|
+
// Build the site
|
|
70
|
+
const build = await import("./build.js");
|
|
71
|
+
if (typeof build.default === "function") {
|
|
72
|
+
await build.default(outDir);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Git operations
|
|
76
|
+
try {
|
|
77
|
+
console.log("Adding files to git...");
|
|
78
|
+
execSync("git add .", { stdio: "inherit" });
|
|
79
|
+
|
|
80
|
+
console.log("Committing changes...");
|
|
81
|
+
execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
|
|
82
|
+
stdio: "inherit",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log("Pushing to remote...");
|
|
86
|
+
execSync("git push", { stdio: "inherit" });
|
|
87
|
+
|
|
88
|
+
console.log("Deployed successfully!");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Deploy failed:", error.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
default: {
|
|
96
|
+
// Treat as sitename for new project creation
|
|
97
|
+
const sitename = command;
|
|
98
|
+
const init = await import("./init.js");
|
|
99
|
+
if (typeof init.default === "function") {
|
|
100
|
+
await init.default(sitename);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
106
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DEFAULT_CONCURRENCY = 16;
|
|
2
|
+
|
|
3
|
+
const getConcurrencyLimit = (value = DEFAULT_CONCURRENCY) => {
|
|
4
|
+
const limit = Number(value);
|
|
5
|
+
return Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_CONCURRENCY;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const mapLimit = async (items, mapper, concurrency = DEFAULT_CONCURRENCY) => {
|
|
9
|
+
const limit = Math.min(getConcurrencyLimit(concurrency), items.length || 1);
|
|
10
|
+
const results = new Array(items.length);
|
|
11
|
+
let index = 0;
|
|
12
|
+
|
|
13
|
+
const workers = Array.from({ length: limit }, async () => {
|
|
14
|
+
while (index < items.length) {
|
|
15
|
+
const currentIndex = index;
|
|
16
|
+
index += 1;
|
|
17
|
+
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
await Promise.all(workers);
|
|
22
|
+
return results;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { getConcurrencyLimit, mapLimit };
|
package/src/config.js
CHANGED
|
@@ -16,6 +16,7 @@ const dirs = {
|
|
|
16
16
|
css: path.join(baseDir, "css"),
|
|
17
17
|
js: path.join(baseDir, "js"),
|
|
18
18
|
partials: path.join(baseDir, "partials"),
|
|
19
|
+
data: path.join(baseDir, "data"),
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
async function loadConfig(dir) {
|
|
@@ -31,6 +32,25 @@ async function loadConfig(dir) {
|
|
|
31
32
|
return {};
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
// Hardcoded defaults (used if not specified in config file)
|
|
36
|
+
const builtInDefaults = {
|
|
37
|
+
default_layout_name: 'default',
|
|
38
|
+
// Reading time calculation
|
|
39
|
+
words_per_minute: 200,
|
|
40
|
+
// Image optimization settings
|
|
41
|
+
max_image_width: 800,
|
|
42
|
+
image_quality: 80,
|
|
43
|
+
// LiveReload and watcher settings
|
|
44
|
+
livereload_port: 35729,
|
|
45
|
+
watcher_delay: 100,
|
|
46
|
+
watcher_interval: 500,
|
|
47
|
+
// Pagination
|
|
48
|
+
default_page_count: 2,
|
|
49
|
+
// Build safety
|
|
50
|
+
build_concurrency: 16,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const loadedConfig = await loadConfig(baseDir);
|
|
54
|
+
const defaultConfig = { ...builtInDefaults, ...loadedConfig };
|
|
35
55
|
|
|
36
|
-
export { baseDir, dirs, defaultConfig, loadConfig };
|
|
56
|
+
export { baseDir, dirs, defaultConfig, loadConfig };
|
package/src/data.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import fsExtra from "fs-extra";
|
|
5
|
+
import { dirs } from "./config.js";
|
|
6
|
+
|
|
7
|
+
let dataCache = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load all data files from the data/ folder
|
|
11
|
+
* Supports .json and .yaml/.yml files
|
|
12
|
+
* File name becomes the key: data/team.json -> data.team
|
|
13
|
+
*/
|
|
14
|
+
async function loadData() {
|
|
15
|
+
if (dataCache !== null) {
|
|
16
|
+
return dataCache;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const data = {};
|
|
20
|
+
|
|
21
|
+
// Check if data directory exists
|
|
22
|
+
if (!(await fsExtra.pathExists(dirs.data))) {
|
|
23
|
+
dataCache = data;
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const files = await fs.readdir(dirs.data);
|
|
29
|
+
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const filePath = path.join(dirs.data, file);
|
|
32
|
+
const stat = await fs.stat(filePath);
|
|
33
|
+
|
|
34
|
+
// Skip directories
|
|
35
|
+
if (stat.isDirectory()) continue;
|
|
36
|
+
|
|
37
|
+
const ext = path.extname(file).toLowerCase();
|
|
38
|
+
const name = path.basename(file, ext);
|
|
39
|
+
|
|
40
|
+
// Only process JSON and YAML files
|
|
41
|
+
if (!['.json', '.yaml', '.yml'].includes(ext)) continue;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
45
|
+
|
|
46
|
+
if (ext === '.json') {
|
|
47
|
+
data[name] = JSON.parse(content);
|
|
48
|
+
} else {
|
|
49
|
+
data[name] = yaml.load(content);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(`Error loading data file ${file}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn(`Error reading data directory: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
dataCache = data;
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clear the data cache (useful for watch mode)
|
|
65
|
+
*/
|
|
66
|
+
function clearDataCache() {
|
|
67
|
+
dataCache = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { loadData, clearDataCache };
|