@daz4126/swifty 2.4.1 → 2.6.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 +48 -11
- package/package.json +4 -4
- package/src/assets.js +54 -1
- package/src/cli.js +3 -0
- package/src/layout.js +10 -4
- package/src/pages.js +2 -1
- package/src/partials.js +2 -1
- package/src/watcher.js +147 -43
package/README.md
CHANGED
|
@@ -2,19 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
## Super Speedy Static Site Generator
|
|
4
4
|
|
|
5
|
-
Swifty
|
|
5
|
+
Swifty uses convention over configuration to make it super simple to build blazingly fast static sites.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Markdown pages** with YAML front matter
|
|
10
|
+
- **Automatic image optimization** to WebP
|
|
11
|
+
- **Layouts and partials** for reusable templates
|
|
12
|
+
- **Auto-injected CSS/JS** from your css/ and js/ folders
|
|
13
|
+
- **Code syntax highlighting** via highlight.js
|
|
14
|
+
- **Tags and navigation** generated automatically
|
|
15
|
+
- **Optional [Turbo](https://turbo.hotwired.dev/)** for SPA-like transitions
|
|
10
16
|
|
|
11
17
|
## Quickstart
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
```bash
|
|
20
|
+
npm install @daz4126/swifty
|
|
21
|
+
npx swifty init
|
|
22
|
+
npx swifty start
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then visit [http://localhost:3000](http://localhost:3000)
|
|
26
|
+
|
|
27
|
+
## Project Structure
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
your-site/
|
|
31
|
+
├── pages/ # Markdown content (folder structure = URLs)
|
|
32
|
+
├── layouts/ # HTML layout templates
|
|
33
|
+
├── partials/ # Reusable content snippets
|
|
34
|
+
├── css/ # Stylesheets (auto-injected)
|
|
35
|
+
├── js/ # JavaScript (auto-injected)
|
|
36
|
+
├── images/ # Images (auto-optimized to WebP)
|
|
37
|
+
├── template.html # Base HTML template
|
|
38
|
+
└── config.yaml # Site configuration
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx swifty init # Create new project structure
|
|
45
|
+
npx swifty build # Build static site to dist/ (for production)
|
|
46
|
+
npx swifty start # Build, watch, and serve at localhost:3000 (for development)
|
|
47
|
+
npx swifty build --out dir # Build to custom output directory
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Development vs Production
|
|
51
|
+
|
|
52
|
+
- **`swifty start`** - For development. Includes live reload (auto-refreshes browser on file changes) and file watching with incremental builds for CSS/JS/images.
|
|
53
|
+
- **`swifty build`** - For production deployment. Produces clean output without any development scripts.
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
See the [full documentation](https://swifty-oo3v.onrender.com/docs) for details on configuration, layouts, partials, and more.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/swifty",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"description": "Super Speedy Static Site Generator",
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"chokidar": "^4.0.0",
|
|
28
29
|
"fs-extra": "^11.2.0",
|
|
29
30
|
"gray-matter": "^4.0.3",
|
|
30
31
|
"highlight.js": "^11.11.1",
|
|
31
32
|
"js-yaml": "^4.1.0",
|
|
33
|
+
"livereload": "^0.10.3",
|
|
32
34
|
"marked": "^14.1.3",
|
|
33
35
|
"marked-highlight": "^2.2.1",
|
|
34
36
|
"serve": "^14.2.4",
|
|
35
37
|
"sharp": "^0.33.5"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"
|
|
39
|
-
"mocha": "^11.1.0",
|
|
40
|
-
"npm-run-all": "^4.1.5"
|
|
40
|
+
"mocha": "^11.1.0"
|
|
41
41
|
},
|
|
42
42
|
"directories": {
|
|
43
43
|
"test": "test"
|
package/src/assets.js
CHANGED
|
@@ -102,4 +102,57 @@ const getJsImports = () =>
|
|
|
102
102
|
validExtensions.js,
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// Copy a single asset file (CSS or JS)
|
|
106
|
+
const copySingleAsset = async (filePath, outputDir = dirs.dist) => {
|
|
107
|
+
const filename = path.basename(filePath);
|
|
108
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
109
|
+
|
|
110
|
+
let destDir;
|
|
111
|
+
if (validExtensions.css.includes(ext)) {
|
|
112
|
+
destDir = path.join(outputDir, "css");
|
|
113
|
+
} else if (validExtensions.js.includes(ext)) {
|
|
114
|
+
destDir = path.join(outputDir, "js");
|
|
115
|
+
} else {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await fsExtra.ensureDir(destDir);
|
|
120
|
+
await fsExtra.copy(filePath, path.join(destDir, filename));
|
|
121
|
+
console.log(`Copied ${filename}`);
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Process a single image
|
|
126
|
+
const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
|
|
127
|
+
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
|
|
128
|
+
const filename = path.basename(filePath);
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
const imagesFolder = path.join(outputDir, "images");
|
|
131
|
+
|
|
132
|
+
await fsExtra.ensureDir(imagesFolder);
|
|
133
|
+
|
|
134
|
+
// For non-optimizable images (svg, webp, gif), just copy
|
|
135
|
+
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
|
136
|
+
await fsExtra.copy(filePath, path.join(imagesFolder, filename));
|
|
137
|
+
console.log(`Copied ${filename}`);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Optimize jpg/jpeg/png to webp
|
|
142
|
+
const optimizedPath = path.join(imagesFolder, `${path.basename(filename, ext)}.webp`);
|
|
143
|
+
const image = sharp(filePath);
|
|
144
|
+
const metadata = await image.metadata();
|
|
145
|
+
const originalWidth = metadata.width || 0;
|
|
146
|
+
const maxWidth = defaultConfig.max_image_size || 800;
|
|
147
|
+
const resizeWidth = Math.min(originalWidth, maxWidth);
|
|
148
|
+
|
|
149
|
+
await image
|
|
150
|
+
.resize({ width: resizeWidth })
|
|
151
|
+
.toFormat("webp", { quality: 80 })
|
|
152
|
+
.toFile(optimizedPath);
|
|
153
|
+
|
|
154
|
+
console.log(`Optimized ${filename} -> ${path.basename(optimizedPath)}`);
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export { copyAssets, optimizeImages, getCssImports, getJsImports, copySingleAsset, optimizeSingleImage };
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,9 @@ async function main() {
|
|
|
29
29
|
break;
|
|
30
30
|
}
|
|
31
31
|
case "start": {
|
|
32
|
+
// Set watch mode so livereload script is injected into pages
|
|
33
|
+
process.env.SWIFTY_WATCH = "true";
|
|
34
|
+
|
|
32
35
|
const build = await import("./build.js");
|
|
33
36
|
if (typeof build.default === "function") {
|
|
34
37
|
await build.default(outDir);
|
package/src/layout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import fsExtra from "fs-extra";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { dirs, baseDir } from "./config.js";
|
|
4
|
+
import { dirs, baseDir, defaultConfig } from "./config.js";
|
|
5
5
|
import { getCssImports, getJsImports } from "./assets.js";
|
|
6
6
|
|
|
7
7
|
const layoutCache = new Map();
|
|
@@ -23,19 +23,25 @@ const createTemplate = async () => {
|
|
|
23
23
|
// Read the template from pages folder
|
|
24
24
|
const templatePath = path.join(baseDir, 'template.html');
|
|
25
25
|
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
26
|
-
const turboScript =
|
|
26
|
+
const turboScript = defaultConfig.turbo
|
|
27
|
+
? `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`
|
|
28
|
+
: '';
|
|
29
|
+
const livereloadScript = process.env.SWIFTY_WATCH
|
|
30
|
+
? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>`
|
|
31
|
+
: '';
|
|
27
32
|
const css = await getCssImports();
|
|
28
33
|
const js = await getJsImports();
|
|
29
34
|
const imports = css + js;
|
|
30
35
|
const highlightCSS = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/monokai-sublime.min.css">`;
|
|
31
|
-
const template = templateContent.replace('</head>', `${turboScript}\n${highlightCSS}\n${imports}\n</head>`);
|
|
36
|
+
const template = templateContent.replace('</head>', `${turboScript}\n${highlightCSS}\n${imports}\n${livereloadScript}\n</head>`);
|
|
32
37
|
return template;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
const applyLayoutAndWrapContent = async (page,content) => {
|
|
36
41
|
const layoutContent = await getLayout(page.data.layout !== undefined ? page.data.layout : page.layout);
|
|
37
42
|
if (!layoutContent) return content;
|
|
38
|
-
|
|
43
|
+
// Use function to avoid $` special replacement patterns in content
|
|
44
|
+
return layoutContent.replace(/\{\{\s*content\s*\}\}/g, () => content);
|
|
39
45
|
};
|
|
40
46
|
|
|
41
47
|
const template = await createTemplate();
|
package/src/pages.js
CHANGED
|
@@ -199,9 +199,10 @@ const render = async (page) => {
|
|
|
199
199
|
const replacedContent = await replacePlaceholders(page.content, page);
|
|
200
200
|
const htmlContent = marked.parse(replacedContent); // Markdown processed once
|
|
201
201
|
const wrappedContent = await applyLayoutAndWrapContent(page, htmlContent);
|
|
202
|
+
// Use function to avoid $` special replacement patterns in content
|
|
202
203
|
const htmlWithTemplate = template.replace(
|
|
203
204
|
/\{\{\s*content\s*\}\}/g,
|
|
204
|
-
wrappedContent,
|
|
205
|
+
() => wrappedContent,
|
|
205
206
|
);
|
|
206
207
|
const finalContent = await replacePlaceholders(htmlWithTemplate, page);
|
|
207
208
|
return finalContent;
|
package/src/partials.js
CHANGED
|
@@ -45,8 +45,9 @@ const replacePlaceholders = async (template, values) => {
|
|
|
45
45
|
},
|
|
46
46
|
);
|
|
47
47
|
// Replace other placeholders **only outside of code blocks**
|
|
48
|
+
// Fenced blocks require closing ``` to be at start of line (after newline)
|
|
48
49
|
const codeBlockRegex =
|
|
49
|
-
/```[\s\S]
|
|
50
|
+
/```[\s\S]*?\n```|`[^`\n]+`|<(pre|code)[^>]*>[\s\S]*?<\/\1>/g;
|
|
50
51
|
const codeBlocks = [];
|
|
51
52
|
template = template.replace(codeBlockRegex, (match) => {
|
|
52
53
|
codeBlocks.push(match);
|
package/src/watcher.js
CHANGED
|
@@ -1,50 +1,154 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
|
-
import
|
|
2
|
+
import livereload from "livereload";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
-
const
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"css
|
|
11
|
-
"js
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
awaitWriteFinish: {
|
|
26
|
-
stabilityThreshold: 200,
|
|
27
|
-
pollInterval: 100,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Rebuild function
|
|
32
|
-
function triggerBuild(event, filePath) {
|
|
33
|
-
console.log(`File ${event}: ${filePath}. Running build...`);
|
|
34
|
-
exec(buildScript, (error, stdout, stderr) => {
|
|
35
|
-
if (error) {
|
|
36
|
-
console.error(`Build failed: ${error.message}`);
|
|
37
|
-
return;
|
|
6
|
+
// Directory to extension mapping for filtering
|
|
7
|
+
const dirExtensions = {
|
|
8
|
+
pages: [".md", ".html"],
|
|
9
|
+
layouts: [".html"],
|
|
10
|
+
images: null, // null means all files
|
|
11
|
+
css: [".css"],
|
|
12
|
+
js: [".js"],
|
|
13
|
+
partials: [".md", ".html"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Build watch paths
|
|
17
|
+
function getWatchPaths() {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const watchPaths = [];
|
|
20
|
+
|
|
21
|
+
// Add directories that exist
|
|
22
|
+
for (const dir of Object.keys(dirExtensions)) {
|
|
23
|
+
const dirPath = path.join(cwd, dir);
|
|
24
|
+
if (fs.existsSync(dirPath)) {
|
|
25
|
+
watchPaths.push(dirPath);
|
|
38
26
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add specific config files that exist
|
|
30
|
+
const configFiles = ["template.html", "config.yaml", "config.yml", "config.json"];
|
|
31
|
+
for (const file of configFiles) {
|
|
32
|
+
const filePath = path.join(cwd, file);
|
|
33
|
+
if (fs.existsSync(filePath)) {
|
|
34
|
+
watchPaths.push(filePath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return watchPaths;
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
// Check if a file should trigger a rebuild
|
|
42
|
+
function shouldTriggerBuild(filePath) {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const relativePath = path.relative(cwd, filePath);
|
|
45
|
+
const parts = relativePath.split(path.sep);
|
|
46
|
+
const topDir = parts[0];
|
|
47
|
+
|
|
48
|
+
// Config files at root level
|
|
49
|
+
if (parts.length === 1) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check against directory extension mapping
|
|
54
|
+
if (dirExtensions[topDir] !== undefined) {
|
|
55
|
+
const allowedExts = dirExtensions[topDir];
|
|
56
|
+
// null means all files allowed
|
|
57
|
+
if (allowedExts === null) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
61
|
+
return allowedExts.includes(ext);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine what type of change this is
|
|
68
|
+
function getChangeType(filePath) {
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const relativePath = path.relative(cwd, filePath);
|
|
71
|
+
const parts = relativePath.split(path.sep);
|
|
72
|
+
const topDir = parts[0];
|
|
73
|
+
|
|
74
|
+
if (topDir === "css") return "css";
|
|
75
|
+
if (topDir === "js") return "js";
|
|
76
|
+
if (topDir === "images") return "image";
|
|
77
|
+
return "full"; // pages, layouts, partials, config files
|
|
78
|
+
}
|
|
49
79
|
|
|
50
|
-
|
|
80
|
+
export default async function watch(outDir = "dist") {
|
|
81
|
+
const build = await import("./build.js");
|
|
82
|
+
const { copySingleAsset, optimizeSingleImage } = await import("./assets.js");
|
|
83
|
+
const watchPaths = getWatchPaths();
|
|
84
|
+
|
|
85
|
+
if (watchPaths.length === 0) {
|
|
86
|
+
console.log("No directories or files to watch.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Start livereload server
|
|
91
|
+
const lrServer = livereload.createServer({
|
|
92
|
+
usePolling: true,
|
|
93
|
+
delay: 100,
|
|
94
|
+
});
|
|
95
|
+
const outPath = path.join(process.cwd(), outDir);
|
|
96
|
+
lrServer.watch(outPath);
|
|
97
|
+
console.log("LiveReload server started on port 35729");
|
|
98
|
+
|
|
99
|
+
// Initialize watcher (chokidar 4.x)
|
|
100
|
+
// usePolling needed for network/cloud drives that don't emit native fs events
|
|
101
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
102
|
+
persistent: true,
|
|
103
|
+
ignoreInitial: true,
|
|
104
|
+
usePolling: true,
|
|
105
|
+
interval: 500,
|
|
106
|
+
awaitWriteFinish: {
|
|
107
|
+
stabilityThreshold: 200,
|
|
108
|
+
pollInterval: 100,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Handle file changes with incremental builds where possible
|
|
113
|
+
async function handleFileChange(event, filePath) {
|
|
114
|
+
if (!shouldTriggerBuild(filePath)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const changeType = getChangeType(filePath);
|
|
119
|
+
const filename = path.basename(filePath);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (event === "deleted") {
|
|
123
|
+
// For deletions, do a full rebuild to clean up
|
|
124
|
+
console.log(`File deleted: ${filename}. Running full build...`);
|
|
125
|
+
await build.default(outDir);
|
|
126
|
+
} else if (changeType === "css" || changeType === "js") {
|
|
127
|
+
// Incremental: just copy the changed asset
|
|
128
|
+
console.log(`Asset ${event}: ${filename}`);
|
|
129
|
+
await copySingleAsset(filePath, outDir);
|
|
130
|
+
} else if (changeType === "image") {
|
|
131
|
+
// Incremental: just process the changed image
|
|
132
|
+
console.log(`Image ${event}: ${filename}`);
|
|
133
|
+
await optimizeSingleImage(filePath, outDir);
|
|
134
|
+
} else {
|
|
135
|
+
// Full rebuild for pages, layouts, partials, config
|
|
136
|
+
console.log(`File ${event}: ${filename}. Running full build...`);
|
|
137
|
+
await build.default(outDir);
|
|
138
|
+
}
|
|
139
|
+
// Trigger browser refresh
|
|
140
|
+
lrServer.refresh("/");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`Build failed: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Set up event listeners
|
|
147
|
+
watcher
|
|
148
|
+
.on("change", (filePath) => handleFileChange("changed", filePath))
|
|
149
|
+
.on("add", (filePath) => handleFileChange("added", filePath))
|
|
150
|
+
.on("unlink", (filePath) => handleFileChange("deleted", filePath))
|
|
151
|
+
.on("error", (error) => console.error("Watcher error:", error));
|
|
152
|
+
|
|
153
|
+
console.log("Watching for file changes...");
|
|
154
|
+
}
|