@daz4126/swifty 3.1.0 → 4.1.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/src/config.js CHANGED
@@ -17,24 +17,105 @@ const dirs = {
17
17
  js: path.join(baseDir, "js"),
18
18
  partials: path.join(baseDir, "partials"),
19
19
  data: path.join(baseDir, "data"),
20
+ public: path.join(baseDir, "public"),
21
+ cache: path.join(baseDir, ".swifty-cache"),
20
22
  };
21
23
 
22
24
  async function loadConfig(dir) {
23
25
  const configFiles = ['config.yaml', 'config.yml', 'config.json'];
24
26
  for (const file of configFiles) {
25
27
  const filePath = path.join(dir, file);
28
+ let content;
29
+
30
+ try {
31
+ content = await fs.readFile(filePath, 'utf-8');
32
+ } catch (error) {
33
+ if (error.code === "ENOENT") continue;
34
+ throw new Error(`Unable to read config ${filePath}: ${error.message}`, {
35
+ cause: error,
36
+ });
37
+ }
38
+
26
39
  try {
27
- await fs.access(filePath);
28
- const content = await fs.readFile(filePath, 'utf-8');
29
- return file.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
30
- } catch {}
40
+ const config = file.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
41
+ return validateConfig(config || {}, filePath);
42
+ } catch (error) {
43
+ throw new Error(`Unable to parse config ${filePath}: ${error.message}`, {
44
+ cause: error,
45
+ });
46
+ }
31
47
  }
32
48
  return {};
33
49
  }
34
50
 
51
+ const validateConfig = (config, filePath = "config") => {
52
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
53
+ throw new TypeError(`${filePath} must contain a configuration object`);
54
+ }
55
+
56
+ const booleans = [
57
+ "minify",
58
+ "minify_html",
59
+ "minify_css",
60
+ "minify_js",
61
+ "morphing",
62
+ "prefetching",
63
+ ];
64
+ const positiveNumbers = [
65
+ "build_concurrency",
66
+ "image_quality",
67
+ "livereload_port",
68
+ "max_image_width",
69
+ "server_port",
70
+ "watcher_delay",
71
+ "watcher_interval",
72
+ "words_per_minute",
73
+ ];
74
+
75
+ for (const key of booleans) {
76
+ if (config[key] !== undefined && typeof config[key] !== "boolean") {
77
+ throw new TypeError(`${key} in ${filePath} must be a boolean`);
78
+ }
79
+ }
80
+ for (const key of positiveNumbers) {
81
+ if (
82
+ config[key] !== undefined &&
83
+ (!Number.isFinite(config[key]) || config[key] <= 0)
84
+ ) {
85
+ throw new TypeError(`${key} in ${filePath} must be a positive number`);
86
+ }
87
+ }
88
+ if (
89
+ config.base_path !== undefined &&
90
+ (typeof config.base_path !== "string" ||
91
+ /^(?:[a-z]+:)?\/\//i.test(config.base_path) ||
92
+ config.base_path.includes("..") ||
93
+ /[?#]/.test(config.base_path))
94
+ ) {
95
+ throw new TypeError(`base_path in ${filePath} must be a URL path`);
96
+ }
97
+ if (
98
+ config.responsive_image_widths !== undefined &&
99
+ (!Array.isArray(config.responsive_image_widths) ||
100
+ config.responsive_image_widths.some(
101
+ (width) => !Number.isInteger(width) || width <= 0,
102
+ ))
103
+ ) {
104
+ throw new TypeError(
105
+ `responsive_image_widths in ${filePath} must contain positive integers`,
106
+ );
107
+ }
108
+ if (config.rss_feeds !== undefined && !Array.isArray(config.rss_feeds)) {
109
+ throw new TypeError(`rss_feeds in ${filePath} must be an array`);
110
+ }
111
+
112
+ return config;
113
+ };
114
+
35
115
  // Hardcoded defaults (used if not specified in config file)
36
116
  const builtInDefaults = {
37
117
  default_layout_name: 'default',
118
+ base_path: '',
38
119
  // Reading time calculation
39
120
  words_per_minute: 200,
40
121
  // Image optimization settings
@@ -49,6 +130,7 @@ const builtInDefaults = {
49
130
  minify_css: true,
50
131
  minify_js: true,
51
132
  // LiveReload and watcher settings
133
+ server_port: 3000,
52
134
  livereload_port: 35729,
53
135
  watcher_delay: 100,
54
136
  watcher_interval: 500,
@@ -64,15 +146,27 @@ const builtInDefaults = {
64
146
  navigation_cache_ttl: 15,
65
147
  };
66
148
 
67
- const loadedConfig = await loadConfig(baseDir);
68
- const hasConfig = (key) => Object.prototype.hasOwnProperty.call(loadedConfig, key);
149
+ const defaultConfig = {};
150
+
151
+ const reloadConfig = async () => {
152
+ const loadedConfig = await loadConfig(baseDir);
153
+ const hasConfig = (key) =>
154
+ Object.prototype.hasOwnProperty.call(loadedConfig, key);
69
155
 
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;
156
+ if (hasConfig('turbo')) {
157
+ console.warn('The "turbo" config option is deprecated. Use "morphing" and "prefetching" instead.');
158
+ if (!hasConfig('morphing')) {
159
+ loadedConfig.morphing = loadedConfig.turbo;
160
+ }
74
161
  }
75
- }
76
- const defaultConfig = { ...builtInDefaults, ...loadedConfig };
77
162
 
78
- export { baseDir, dirs, defaultConfig, loadConfig };
163
+ for (const key of Object.keys(defaultConfig)) {
164
+ delete defaultConfig[key];
165
+ }
166
+ Object.assign(defaultConfig, builtInDefaults, loadedConfig);
167
+ return defaultConfig;
168
+ };
169
+
170
+ await reloadConfig();
171
+
172
+ export { baseDir, dirs, defaultConfig, loadConfig, reloadConfig, validateConfig };
package/src/data.js CHANGED
@@ -24,36 +24,41 @@ async function loadData() {
24
24
  return data;
25
25
  }
26
26
 
27
+ let files;
27
28
  try {
28
- const files = await fs.readdir(dirs.data);
29
+ files = await fs.readdir(dirs.data);
30
+ } catch (error) {
31
+ throw new Error(`Unable to read data directory ${dirs.data}: ${error.message}`, {
32
+ cause: error,
33
+ });
34
+ }
29
35
 
30
- for (const file of files) {
31
- const filePath = path.join(dirs.data, file);
32
- const stat = await fs.stat(filePath);
36
+ for (const file of files) {
37
+ const filePath = path.join(dirs.data, file);
38
+ const stat = await fs.stat(filePath);
33
39
 
34
- // Skip directories
35
- if (stat.isDirectory()) continue;
40
+ // Skip directories
41
+ if (stat.isDirectory()) continue;
36
42
 
37
- const ext = path.extname(file).toLowerCase();
38
- const name = path.basename(file, ext);
43
+ const ext = path.extname(file).toLowerCase();
44
+ const name = path.basename(file, ext);
39
45
 
40
- // Only process JSON and YAML files
41
- if (!['.json', '.yaml', '.yml'].includes(ext)) continue;
46
+ // Only process JSON and YAML files
47
+ if (!['.json', '.yaml', '.yml'].includes(ext)) continue;
42
48
 
43
- try {
44
- const content = await fs.readFile(filePath, 'utf-8');
49
+ try {
50
+ const content = await fs.readFile(filePath, 'utf-8');
45
51
 
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}`);
52
+ if (ext === '.json') {
53
+ data[name] = JSON.parse(content);
54
+ } else {
55
+ data[name] = yaml.load(content);
53
56
  }
57
+ } catch (error) {
58
+ throw new Error(`Unable to parse data file ${filePath}: ${error.message}`, {
59
+ cause: error,
60
+ });
54
61
  }
55
- } catch (error) {
56
- console.warn(`Error reading data directory: ${error.message}`);
57
62
  }
58
63
 
59
64
  dataCache = data;
@@ -0,0 +1,27 @@
1
+ import yaml from "js-yaml";
2
+
3
+ const frontMatterPattern =
4
+ /^---[ \t]*\r?\n([\s\S]*?)^---[ \t]*(?:\r?\n|$)/m;
5
+
6
+ const parseFrontMatter = (source) => {
7
+ if (!/^---[ \t]*(?:\r?\n|$)/.test(source)) {
8
+ return { data: {}, content: source };
9
+ }
10
+
11
+ const match = source.match(frontMatterPattern);
12
+ if (!match) {
13
+ throw new Error("Front matter is missing a closing --- delimiter");
14
+ }
15
+
16
+ const data = yaml.load(match[1]) || {};
17
+ if (typeof data !== "object" || Array.isArray(data)) {
18
+ throw new TypeError("Front matter must contain a YAML object");
19
+ }
20
+
21
+ return {
22
+ data,
23
+ content: source.slice(match[0].length),
24
+ };
25
+ };
26
+
27
+ export { parseFrontMatter };
package/src/index.js ADDED
@@ -0,0 +1,10 @@
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";
4
+ export { startServer } from "./server.js";
5
+ export {
6
+ applyBasePathToHtml,
7
+ normalizeBasePath,
8
+ normalizePermalink,
9
+ withBasePath,
10
+ } from "./urls.js";
package/src/init.js CHANGED
@@ -12,7 +12,11 @@ function getStructure(sitename) {
12
12
  "js/": null,
13
13
  "images/": null,
14
14
  "data/": null,
15
+ "public/": null,
16
+ ".gitignore": `node_modules/
17
+ .swifty-cache/`,
15
18
  "config.yaml": `sitename: ${sitename}
19
+ base_path: ""
16
20
  breadcrumb_separator: "&raquo;"
17
21
  breadcrumb_class: swifty_breadcrumb
18
22
  link_class: swifty_link
package/src/layout.js CHANGED
@@ -37,7 +37,14 @@ const getLayout = async (layoutName) => {
37
37
  const createTemplate = async () => {
38
38
  // Read the template from pages folder
39
39
  const templatePath = path.join(baseDir, 'template.html');
40
- const templateContent = await fs.readFile(templatePath, 'utf-8');
40
+ let templateContent;
41
+ try {
42
+ templateContent = await fs.readFile(templatePath, 'utf-8');
43
+ } catch (error) {
44
+ throw new Error(`Unable to read template ${templatePath}: ${error.message}`, {
45
+ cause: error,
46
+ });
47
+ }
41
48
 
42
49
  // Preconnect hints for external CDNs (improves connection setup time)
43
50
  const preconnectHints = [
package/src/minify.js CHANGED
@@ -1,129 +1,44 @@
1
- const protectBlocks = (content, pattern) => {
2
- const blocks = [];
3
- const protectedContent = content.replace(pattern, (match) => {
4
- const token = `__SWIFTY_MINIFY_BLOCK_${blocks.length}__`;
5
- blocks.push(match);
6
- return token;
7
- });
8
-
9
- return {
10
- content: protectedContent,
11
- restore: (value) =>
12
- value.replace(/__SWIFTY_MINIFY_BLOCK_(\d+)__/g, (_, index) => blocks[index]),
13
- };
14
- };
15
-
16
- const protectCssValues = (css) => {
17
- const blocks = [];
18
- let content = "";
19
-
20
- const protect = (start, end) => {
21
- const token = `__SWIFTY_MINIFY_BLOCK_${blocks.length}__`;
22
- blocks.push(css.slice(start, end));
23
- content += token;
24
- return end;
25
- };
26
-
27
- const readStringEnd = (start) => {
28
- const quote = css[start];
29
- let index = start + 1;
30
-
31
- while (index < css.length) {
32
- if (css[index] === "\\") {
33
- index += 2;
34
- continue;
35
- }
36
- if (css[index] === quote) return index + 1;
37
- index += 1;
38
- }
39
-
40
- return css.length;
41
- };
42
-
43
- const readUrlEnd = (start) => {
44
- let index = start + 4;
45
-
46
- while (index < css.length) {
47
- const char = css[index];
48
- if (char === "\"" || char === "'") {
49
- index = readStringEnd(index);
50
- continue;
51
- }
52
- if (char === "\\") {
53
- index += 2;
54
- continue;
55
- }
56
- if (char === ")") return index + 1;
57
- index += 1;
58
- }
59
-
60
- return css.length;
61
- };
62
-
63
- for (let index = 0; index < css.length; ) {
64
- const char = css[index];
65
- const isUrl = css.slice(index, index + 4).toLowerCase() === "url(";
66
-
67
- if (char === "/" && css[index + 1] === "*") {
68
- const commentEnd = css.indexOf("*/", index + 2);
69
- const end = commentEnd === -1 ? css.length : commentEnd + 2;
70
- content += css.slice(index, end);
71
- index = end;
72
- } else if (char === "\"" || char === "'") {
73
- index = protect(index, readStringEnd(index));
74
- } else if (isUrl) {
75
- index = protect(index, readUrlEnd(index));
76
- } else {
77
- content += char;
78
- index += 1;
79
- }
80
- }
81
-
82
- return {
83
- content,
84
- restore: (value) =>
85
- value.replace(/__SWIFTY_MINIFY_BLOCK_(\d+)__/g, (_, index) => blocks[index]),
86
- };
87
- };
1
+ import CleanCSS from "clean-css";
2
+ import { minify as minifyHtmlContent } from "html-minifier-terser";
3
+ import { minify as minifyJsContent } from "terser";
88
4
 
89
5
  const minifyCss = (css) => {
90
- const values = protectCssValues(css);
6
+ const result = new CleanCSS({
7
+ level: 1,
8
+ rebase: false,
9
+ }).minify(css);
91
10
 
92
- const minified = values.content
93
- .replace(/\/\*[\s\S]*?\*\//g, "")
94
- .replace(/\s+/g, " ")
95
- .replace(/\s*([{}:;,])\s*/g, "$1")
96
- .replace(/;}/g, "}")
97
- .trim();
11
+ if (result.errors.length) {
12
+ throw new Error(result.errors.join("; "));
13
+ }
98
14
 
99
- return values.restore(minified);
15
+ return result.styles;
100
16
  };
101
17
 
102
- const minifyJs = (js) => {
103
- const strings = protectBlocks(js, /(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g);
104
-
105
- const minified = strings.content
106
- .split("\n")
107
- .map((line) => line.trim())
108
- .filter(Boolean)
109
- .join("\n");
18
+ const minifyJs = async (js) => {
19
+ const result = await minifyJsContent(js, {
20
+ compress: true,
21
+ mangle: false,
22
+ format: {
23
+ comments: /^!/,
24
+ },
25
+ });
110
26
 
111
- return strings.restore(minified);
27
+ return result.code || "";
112
28
  };
113
29
 
114
- const minifyHtml = (html) => {
115
- const blocks = protectBlocks(
116
- html,
117
- /<(pre|code|textarea|script|style)\b[^>]*>[\s\S]*?<\/\1>/gi,
118
- );
119
-
120
- const minified = blocks.content
121
- .replace(/<!--(?!\[if|<!|>)[\s\S]*?-->/g, "")
122
- .replace(/\s{2,}/g, " ")
123
- .replace(/>\s+</g, "><")
124
- .trim();
125
-
126
- return blocks.restore(minified);
127
- };
30
+ const minifyHtml = (html) =>
31
+ minifyHtmlContent(html, {
32
+ caseSensitive: true,
33
+ collapseWhitespace: true,
34
+ conservativeCollapse: true,
35
+ continueOnParseError: false,
36
+ keepClosingSlash: true,
37
+ minifyCSS: false,
38
+ minifyJS: false,
39
+ removeComments: true,
40
+ removeScriptTypeAttributes: true,
41
+ removeStyleLinkTypeAttributes: true,
42
+ });
128
43
 
129
44
  export { minifyCss, minifyHtml, minifyJs };