@daz4126/swifty 2.5.0 → 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 CHANGED
@@ -2,19 +2,56 @@
2
2
 
3
3
  ## Super Speedy Static Site Generator
4
4
 
5
- Swifty is the next generation of static site generator.
5
+ Swifty uses convention over configuration to make it super simple to build blazingly fast static sites.
6
6
 
7
- It uses the power of Turbo and morphing to build sites that are blazingly fast.
7
+ ## Features
8
8
 
9
- It also uses convention over configuration to make is super simple to build sites.
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
- 1. `npm install @daz4126/swifty`
14
- 2. `npx swifty init` to create a new site
15
- 3. Edit the `template.html` file to match your default layout
16
- 4. Change the `sitename` in `config.yaml`
17
- 5. Add some markdown files to the 'pages' directory
18
- 3. `npx swifty build` to build the site
19
- 7. `npx swifty start` to start the server
20
- 8. Visit [http://localhost:3000](http://localhost:3000) to see your site
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.5.0",
3
+ "version": "2.6.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,15 +30,14 @@
30
30
  "gray-matter": "^4.0.3",
31
31
  "highlight.js": "^11.11.1",
32
32
  "js-yaml": "^4.1.0",
33
+ "livereload": "^0.10.3",
33
34
  "marked": "^14.1.3",
34
35
  "marked-highlight": "^2.2.1",
35
36
  "serve": "^14.2.4",
36
37
  "sharp": "^0.33.5"
37
38
  },
38
39
  "devDependencies": {
39
- "chokidar-cli": "^3.0.0",
40
- "mocha": "^11.1.0",
41
- "npm-run-all": "^4.1.5"
40
+ "mocha": "^11.1.0"
42
41
  },
43
42
  "directories": {
44
43
  "test": "test"
package/src/assets.js CHANGED
@@ -102,4 +102,57 @@ const getJsImports = () =>
102
102
  validExtensions.js,
103
103
  );
104
104
 
105
- export { copyAssets, optimizeImages, getCssImports, getJsImports };
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 = `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`;
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
- return layoutContent.replace(/\{\{\s*content\s*\}\}/g, content);
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/watcher.js CHANGED
@@ -1,39 +1,143 @@
1
1
  import chokidar from "chokidar";
2
+ import livereload from "livereload";
2
3
  import path from "path";
4
+ import fs from "fs";
3
5
 
4
- // Define files to watch, resolving relative to the current working directory
5
- const filesToWatch = [
6
- "pages/**/*.{md,html}",
7
- "layouts/**/*.html",
8
- "images/**/*",
9
- "css/**/*.css",
10
- "js/**/*.js",
11
- "partials/**/*.{md,html}",
12
- "template.html",
13
- "config.yaml",
14
- "config.yml",
15
- "config.json",
16
- ].map((pattern) => path.join(process.cwd(), pattern));
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);
26
+ }
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;
39
+ }
40
+
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
+ }
17
79
 
18
80
  export default async function watch(outDir = "dist") {
19
81
  const build = await import("./build.js");
82
+ const { copySingleAsset, optimizeSingleImage } = await import("./assets.js");
83
+ const watchPaths = getWatchPaths();
20
84
 
21
- // Initialize watcher
22
- const watcher = chokidar.watch(filesToWatch, {
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, {
23
102
  persistent: true,
24
103
  ignoreInitial: true,
104
+ usePolling: true,
105
+ interval: 500,
25
106
  awaitWriteFinish: {
26
107
  stabilityThreshold: 200,
27
108
  pollInterval: 100,
28
109
  },
29
110
  });
30
111
 
31
- // Rebuild function
32
- async function triggerBuild(event, filePath) {
33
- console.log(`File ${event}: ${filePath}. Running build...`);
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
+
34
121
  try {
35
- await build.default(outDir);
36
- console.log("Build complete.");
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("/");
37
141
  } catch (error) {
38
142
  console.error(`Build failed: ${error.message}`);
39
143
  }
@@ -41,9 +145,10 @@ export default async function watch(outDir = "dist") {
41
145
 
42
146
  // Set up event listeners
43
147
  watcher
44
- .on("change", (filePath) => triggerBuild("changed", filePath))
45
- .on("add", (filePath) => triggerBuild("added", filePath))
46
- .on("unlink", (filePath) => triggerBuild("deleted", filePath));
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));
47
152
 
48
153
  console.log("Watching for file changes...");
49
154
  }