@daz4126/swifty 3.1.0 → 3.2.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
@@ -59,14 +59,14 @@ npx swifty my-site # Create new site in my-site/ folder
59
59
  npx swifty build # Build static site to dist/ (for production)
60
60
  npx swifty start # Build, watch, and serve at localhost:3000 (for development)
61
61
  npx swifty build --out dir # Build to custom output directory
62
- npx swifty deploy "message" # Build, git add, commit, and push (message optional)
62
+ npx swifty deploy "message" # Build, commit the output folder, and push
63
63
  ```
64
64
 
65
65
  ### Development vs Production
66
66
 
67
67
  - **`swifty start`** - For development. Includes live reload (auto-refreshes browser on file changes) and file watching with incremental builds for CSS/JS/images.
68
68
  - **`swifty build`** - For production deployment. Produces clean output without any development scripts.
69
- - **`swifty deploy "message"`** - Builds the site and commits/pushes to git in one command.
69
+ - **`swifty deploy "message"`** - Builds the site, commits only the generated output folder, and pushes it to git.
70
70
 
71
71
  ## Documentation
72
72
 
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@daz4126/swifty",
3
- "version": "3.1.0",
4
- "main": "index.js",
3
+ "version": "3.2.0",
4
+ "main": "./src/index.js",
5
5
  "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
6
9
  "bin": {
7
10
  "swifty": "./src/cli.js"
8
11
  },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
9
15
  "files": [
10
16
  "src/"
11
17
  ],
@@ -45,10 +51,10 @@
45
51
  },
46
52
  "repository": {
47
53
  "type": "git",
48
- "url": "git+https://github.com/daz4126/swifty.git"
54
+ "url": "git+https://github.com/daz-codes/swifty.git"
49
55
  },
50
56
  "bugs": {
51
- "url": "https://github.com/daz4126/swifty/issues"
57
+ "url": "https://github.com/daz-codes/swifty/issues"
52
58
  },
53
- "homepage": "https://github.com/daz4126/swifty#readme"
59
+ "homepage": "https://github.com/daz-codes/swifty#readme"
54
60
  }
package/src/build.js CHANGED
@@ -1,22 +1,47 @@
1
1
  import { argv } from "process";
2
+ import path from "path";
2
3
  import { fileURLToPath } from "url";
4
+
5
+ import fsExtra from "fs-extra";
6
+
3
7
  import { copyAssets, optimizeImages } from "./assets.js";
4
8
  import { generatePages, createPages, addLinks } from "./pages.js";
5
9
  import { generateRssFeeds } from "./rss.js";
6
10
  import { generateSeoFiles } from "./sitemap.js";
7
- import { dirs } from "./config.js";
11
+ import { baseDir, dirs } from "./config.js";
12
+ import { clearDataCache } from "./data.js";
13
+ import { resetCaches } from "./layout.js";
14
+ import { clearCache as clearPartialCache } from "./partials.js";
15
+
16
+ const prepareOutputDirectory = async (outputDir) => {
17
+ const projectPath = path.resolve(baseDir);
18
+ const outputPath = path.resolve(baseDir, outputDir);
19
+ const outputContainsProject =
20
+ projectPath === outputPath || projectPath.startsWith(`${outputPath}${path.sep}`);
21
+
22
+ if (outputContainsProject) {
23
+ throw new Error(`Refusing to empty unsafe output directory: ${outputPath}`);
24
+ }
25
+
26
+ await fsExtra.emptyDir(outputPath);
27
+ return outputPath;
28
+ };
8
29
 
9
30
  export default async function build(outputDir = dirs.dist) {
10
31
  const startTime = performance.now();
11
32
  console.log("🚀 Starting build...");
33
+ const resolvedOutputDir = await prepareOutputDirectory(outputDir);
34
+ clearDataCache();
35
+ clearPartialCache();
12
36
 
13
37
  const copyStart = performance.now();
14
- await copyAssets(outputDir);
38
+ await copyAssets(resolvedOutputDir);
15
39
  const copyTime = performance.now() - copyStart;
16
40
  console.log(`📁 Assets copied in ${(copyTime / 1000).toFixed(2)}s`);
41
+ await resetCaches();
17
42
 
18
43
  const imageStart = performance.now();
19
- await optimizeImages(outputDir);
44
+ await optimizeImages(resolvedOutputDir);
20
45
  const imageTime = performance.now() - imageStart;
21
46
  console.log(`🖼️ Images optimized in ${(imageTime / 1000).toFixed(2)}s`);
22
47
 
@@ -31,17 +56,17 @@ export default async function build(outputDir = dirs.dist) {
31
56
  console.log(`🔗 Links added in ${(linksTime / 1000).toFixed(2)}s`);
32
57
 
33
58
  const createStart = performance.now();
34
- await createPages(pages, outputDir);
59
+ await createPages(pages, resolvedOutputDir);
35
60
  const createTime = performance.now() - createStart;
36
61
  console.log(`✨ Pages created in ${(createTime / 1000).toFixed(2)}s`);
37
62
 
38
63
  const rssStart = performance.now();
39
- await generateRssFeeds(pages, outputDir);
64
+ await generateRssFeeds(pages, resolvedOutputDir);
40
65
  const rssTime = performance.now() - rssStart;
41
66
  console.log(`📡 RSS feeds generated in ${(rssTime / 1000).toFixed(2)}s`);
42
67
 
43
68
  const seoStart = performance.now();
44
- await generateSeoFiles(pages, outputDir);
69
+ await generateSeoFiles(pages, resolvedOutputDir);
45
70
  const seoTime = performance.now() - seoStart;
46
71
  console.log(`🧭 SEO files generated in ${(seoTime / 1000).toFixed(2)}s`);
47
72
 
@@ -70,6 +95,8 @@ export default async function build(outputDir = dirs.dist) {
70
95
  }
71
96
  }
72
97
 
98
+ export { prepareOutputDirectory };
99
+
73
100
  // Run the build when invoked directly (e.g. `npm run build`, `npm start`),
74
101
  // not when imported as a module (e.g. by cli.js).
75
102
  if (argv[1] && fileURLToPath(import.meta.url) === argv[1]) {
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn, execSync } from "child_process";
3
+ import { execFileSync, spawn, spawnSync } from "child_process";
4
+ import { fileURLToPath } from "url";
4
5
 
5
6
  const args = process.argv.slice(2);
6
7
  let outDir = "dist"; // default
@@ -14,7 +15,46 @@ if (outIndex !== -1 && args[outIndex + 1]) {
14
15
  // Pass outDir as an environment variable too (optional, still useful)
15
16
  process.env.OUT_DIR = outDir;
16
17
 
17
- const reservedCommands = ["build", "start", "watch", "deploy"];
18
+ const commitAndPushOutput = (
19
+ outputDir,
20
+ commitMessage,
21
+ { execFile = execFileSync, spawnFile = spawnSync } = {},
22
+ ) => {
23
+ const existingDiff = spawnFile("git", ["diff", "--cached", "--quiet"], {
24
+ stdio: "ignore",
25
+ });
26
+ if (existingDiff.error) throw existingDiff.error;
27
+ if (existingDiff.status === 1) {
28
+ throw new Error("Refusing to deploy while unrelated changes are already staged.");
29
+ }
30
+ if (existingDiff.status !== 0) {
31
+ throw new Error(
32
+ `Unable to inspect staged changes (git exited ${existingDiff.status}).`,
33
+ );
34
+ }
35
+
36
+ console.log(`Adding generated output from ${outputDir} to git...`);
37
+ execFile("git", ["add", "--", outputDir], { stdio: "inherit" });
38
+
39
+ const diffResult = spawnFile("git", ["diff", "--cached", "--quiet"], {
40
+ stdio: "ignore",
41
+ });
42
+ if (diffResult.error) throw diffResult.error;
43
+ if (diffResult.status === 0) {
44
+ console.log("No generated output changes to deploy.");
45
+ return false;
46
+ }
47
+ if (diffResult.status !== 1) {
48
+ throw new Error(`Unable to inspect staged changes (git exited ${diffResult.status}).`);
49
+ }
50
+
51
+ console.log("Committing generated output...");
52
+ execFile("git", ["commit", "-m", commitMessage], { stdio: "inherit" });
53
+
54
+ console.log("Pushing to remote...");
55
+ execFile("git", ["push"], { stdio: "inherit" });
56
+ return true;
57
+ };
18
58
 
19
59
  async function main() {
20
60
  const command = args[0];
@@ -64,7 +104,11 @@ async function main() {
64
104
  break;
65
105
  }
66
106
  case "deploy": {
67
- const commitMessage = args[1] || "Deploying latest build";
107
+ const commandArgs = args.slice(1).filter((arg, index, values) => {
108
+ if (arg === "--out") return false;
109
+ return index === 0 || values[index - 1] !== "--out";
110
+ });
111
+ const commitMessage = commandArgs[0] || "Deploying latest build";
68
112
 
69
113
  // Build the site
70
114
  const build = await import("./build.js");
@@ -74,18 +118,8 @@ async function main() {
74
118
 
75
119
  // Git operations
76
120
  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!");
121
+ const deployed = commitAndPushOutput(outDir, commitMessage);
122
+ if (deployed) console.log("Deployed successfully!");
89
123
  } catch (error) {
90
124
  console.error("Deploy failed:", error.message);
91
125
  process.exit(1);
@@ -104,4 +138,8 @@ async function main() {
104
138
  }
105
139
  }
106
140
 
107
- main();
141
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
142
+ main();
143
+ }
144
+
145
+ export { commitAndPushOutput, main };
package/src/config.js CHANGED
@@ -64,15 +64,27 @@ const builtInDefaults = {
64
64
  navigation_cache_ttl: 15,
65
65
  };
66
66
 
67
- const loadedConfig = await loadConfig(baseDir);
68
- const hasConfig = (key) => Object.prototype.hasOwnProperty.call(loadedConfig, key);
67
+ const defaultConfig = {};
69
68
 
70
- if (hasConfig('turbo')) {
71
- console.warn('The "turbo" config option is deprecated. Use "morphing" and "prefetching" instead.');
72
- if (!hasConfig('morphing')) {
73
- loadedConfig.morphing = loadedConfig.turbo;
69
+ const reloadConfig = async () => {
70
+ const loadedConfig = await loadConfig(baseDir);
71
+ const hasConfig = (key) =>
72
+ Object.prototype.hasOwnProperty.call(loadedConfig, key);
73
+
74
+ if (hasConfig('turbo')) {
75
+ console.warn('The "turbo" config option is deprecated. Use "morphing" and "prefetching" instead.');
76
+ if (!hasConfig('morphing')) {
77
+ loadedConfig.morphing = loadedConfig.turbo;
78
+ }
74
79
  }
75
- }
76
- const defaultConfig = { ...builtInDefaults, ...loadedConfig };
77
80
 
78
- export { baseDir, dirs, defaultConfig, loadConfig };
81
+ for (const key of Object.keys(defaultConfig)) {
82
+ delete defaultConfig[key];
83
+ }
84
+ Object.assign(defaultConfig, builtInDefaults, loadedConfig);
85
+ return defaultConfig;
86
+ };
87
+
88
+ await reloadConfig();
89
+
90
+ export { baseDir, dirs, defaultConfig, loadConfig, reloadConfig };
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default, default as build, prepareOutputDirectory } from "./build.js";
2
+ export { defaultConfig, loadConfig, reloadConfig } from "./config.js";
3
+ export { addLinks, createPages, generatePages } from "./pages.js";
package/src/minify.js CHANGED
@@ -1,7 +1,10 @@
1
+ let protectionSequence = 0;
2
+
1
3
  const protectBlocks = (content, pattern) => {
2
4
  const blocks = [];
5
+ const tokenPrefix = `__SWIFTY_MINIFY_BLOCK_${protectionSequence++}_`;
3
6
  const protectedContent = content.replace(pattern, (match) => {
4
- const token = `__SWIFTY_MINIFY_BLOCK_${blocks.length}__`;
7
+ const token = `${tokenPrefix}${blocks.length}__`;
5
8
  blocks.push(match);
6
9
  return token;
7
10
  });
@@ -9,7 +12,10 @@ const protectBlocks = (content, pattern) => {
9
12
  return {
10
13
  content: protectedContent,
11
14
  restore: (value) =>
12
- value.replace(/__SWIFTY_MINIFY_BLOCK_(\d+)__/g, (_, index) => blocks[index]),
15
+ value.replace(
16
+ new RegExp(`${tokenPrefix}(\\d+)__`, "g"),
17
+ (_, index) => blocks[index],
18
+ ),
13
19
  };
14
20
  };
15
21
 
@@ -116,14 +122,17 @@ const minifyHtml = (html) => {
116
122
  html,
117
123
  /<(pre|code|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1>/gi,
118
124
  );
125
+ const strings = protectBlocks(
126
+ blocks.content,
127
+ /(["'])(?:\\[\s\S]|(?!\1)[^\\])*\1/g,
128
+ );
119
129
 
120
- const minified = blocks.content
130
+ const minified = strings.content
121
131
  .replace(/<!--(?!\[if|<!|>)[\s\S]*?-->/g, "")
122
- .replace(/\s{2,}/g, " ")
123
- .replace(/>\s+</g, "><")
132
+ .replace(/\s+/g, " ")
124
133
  .trim();
125
134
 
126
- return blocks.restore(minified);
135
+ return blocks.restore(strings.restore(minified));
127
136
  };
128
137
 
129
138
  export { minifyCss, minifyHtml, minifyJs };
package/src/pages.js CHANGED
@@ -44,6 +44,54 @@ const parseDate = (dateValue) => {
44
44
  return isNaN(parsed.getTime()) ? null : parsed;
45
45
  };
46
46
 
47
+ const applyMarkdownFileToPage = async (
48
+ page,
49
+ filePath,
50
+ { notFound = false, folderIndex = false } = {},
51
+ ) => {
52
+ const markdownContent = await fs.readFile(filePath, "utf-8");
53
+ const { data, content } = matter(markdownContent);
54
+ Object.assign(page, { meta: { ...page.meta, ...data }, content });
55
+
56
+ if (folderIndex) {
57
+ const stats = await fs.stat(filePath);
58
+ const { dateFormat } = page.meta;
59
+ page.hasIndexContent = true;
60
+ page.indexFilePath = filePath;
61
+ page.createdAtObj = stats.birthtime;
62
+ page.updatedAtObj = stats.mtime;
63
+ page.created_at = new Date(stats.birthtime).toLocaleDateString(
64
+ undefined,
65
+ dateFormat,
66
+ );
67
+ page.updated_at = new Date(stats.mtime).toLocaleDateString(
68
+ undefined,
69
+ dateFormat,
70
+ );
71
+ page.date = new Date(stats.birthtime).toLocaleDateString(undefined, dateFormat);
72
+ }
73
+
74
+ if (notFound && data.sitemap === undefined) {
75
+ page.meta.sitemap = false;
76
+ }
77
+
78
+ page.title = page.meta.title || page.title;
79
+ page.name = page.meta.title || page.name;
80
+
81
+ if (typeof data.nav === "boolean") {
82
+ page.nav = data.nav;
83
+ }
84
+
85
+ if (data.date) {
86
+ const parsedDate = parseDate(data.date);
87
+ if (parsedDate) {
88
+ page.dateObj = parsedDate;
89
+ page.date = parsedDate.toLocaleDateString(undefined, page.meta.dateFormat);
90
+ page.meta.date = page.date;
91
+ }
92
+ }
93
+ };
94
+
47
95
  const tagsMap = new Map();
48
96
  const pageIndex = [];
49
97
  const pageIndexUrls = new Set();
@@ -78,6 +126,8 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
78
126
  files,
79
127
  async (file) => {
80
128
  const filePath = path.join(sourceDir, file.name);
129
+ if (parent && file.name === "index.md") return null;
130
+
81
131
  const stats = await getValidStats(filePath);
82
132
  if (!stats) return null;
83
133
 
@@ -130,28 +180,11 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
130
180
  };
131
181
 
132
182
  if (path.extname(file.name) === ".md") {
133
- const markdownContent = await fs.readFile(filePath, "utf-8");
134
- const { data, content } = matter(markdownContent);
135
- Object.assign(page, { meta: { ...page.meta, ...data }, content });
136
- if (notFound && data.sitemap === undefined) {
137
- page.meta.sitemap = false;
138
- }
139
- page.title = page.meta.title || page.title;
140
- page.name = page.meta.title || page.name;
141
-
142
- // Allow front matter to override nav (opt in or opt out)
143
- if (typeof data.nav === "boolean") {
144
- page.nav = data.nav;
145
- }
146
-
147
- // If front matter has a date, parse and format it
148
- if (data.date) {
149
- const parsedDate = parseDate(data.date);
150
- if (parsedDate) {
151
- page.dateObj = parsedDate;
152
- page.date = parsedDate.toLocaleDateString(undefined, dateFormat);
153
- page.meta.date = page.date;
154
- }
183
+ await applyMarkdownFileToPage(page, filePath, { notFound });
184
+ } else if (isDirectory) {
185
+ const indexPath = path.join(filePath, "index.md");
186
+ if (await fsExtra.pathExists(indexPath)) {
187
+ await applyMarkdownFileToPage(page, indexPath, { folderIndex: true });
155
188
  }
156
189
  }
157
190
 
@@ -208,7 +241,9 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
208
241
  page.visiblePages = chunks[0];
209
242
 
210
243
  // First page content
211
- page.content = await generateLinkList(page.filename, chunks[0]);
244
+ if (!page.hasIndexContent) {
245
+ page.content = await generateLinkList(page.filename, chunks[0]);
246
+ }
212
247
  page.meta.pagination = generatePaginationNav({
213
248
  currentPage: 1,
214
249
  totalPages,
@@ -242,7 +277,9 @@ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
242
277
  page.paginatedPages.push(paginatedPage);
243
278
  }
244
279
  } else {
245
- page.content = await generateLinkList(page.filename, page.pages);
280
+ if (!page.hasIndexContent) {
281
+ page.content = await generateLinkList(page.filename, page.pages);
282
+ }
246
283
  page.meta.pagination = '';
247
284
  }
248
285
  }
package/src/watcher.js CHANGED
@@ -2,7 +2,7 @@ import chokidar from "chokidar";
2
2
  import livereload from "livereload";
3
3
  import path from "path";
4
4
  import fs from "fs";
5
- import { defaultConfig } from "./config.js";
5
+ import { defaultConfig, reloadConfig } from "./config.js";
6
6
 
7
7
  // Directory to extension mapping for filtering
8
8
  const dirExtensions = {
@@ -80,6 +80,14 @@ function getChangeType(filePath) {
80
80
  return "full"; // pages, layouts, partials, config files
81
81
  }
82
82
 
83
+ const isRootConfigFile = (filePath) => {
84
+ const relativePath = path.relative(process.cwd(), filePath);
85
+ return (
86
+ !relativePath.includes(path.sep) &&
87
+ ["config.yaml", "config.yml", "config.json"].includes(relativePath)
88
+ );
89
+ };
90
+
83
91
  export default async function watch(outDir = "dist") {
84
92
  const build = await import("./build.js");
85
93
  const { copySingleAsset, optimizeSingleImage } = await import("./assets.js");
@@ -94,12 +102,13 @@ export default async function watch(outDir = "dist") {
94
102
  }
95
103
 
96
104
  // Start livereload server
105
+ const livereloadPort = defaultConfig.livereload_port || 35729;
97
106
  const lrServer = livereload.createServer({
107
+ port: livereloadPort,
98
108
  usePolling: true,
99
109
  delay: defaultConfig.watcher_delay || 100,
100
110
  });
101
111
  const outPath = path.join(process.cwd(), outDir);
102
- const livereloadPort = defaultConfig.livereload_port || 35729;
103
112
  lrServer.watch(outPath);
104
113
  console.log(`LiveReload server started on port ${livereloadPort}`);
105
114
 
@@ -128,6 +137,10 @@ export default async function watch(outDir = "dist") {
128
137
  const filename = path.basename(filePath);
129
138
 
130
139
  try {
140
+ if (isRootConfigFile(filePath)) {
141
+ await reloadConfig();
142
+ }
143
+
131
144
  if (event === "deleted") {
132
145
  // For deletions, do a full rebuild to clean up
133
146
  console.log(`🗑️ File deleted: ${filename}. Running full build...`);