@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/MIGRATION.md +60 -0
- package/README.md +7 -2
- package/package.json +20 -10
- package/src/assets.js +124 -29
- package/src/build.js +40 -6
- package/src/cli.js +59 -18
- package/src/config.js +107 -13
- package/src/data.js +26 -21
- package/src/frontmatter.js +27 -0
- package/src/index.js +10 -0
- package/src/init.js +4 -0
- package/src/layout.js +8 -1
- package/src/minify.js +33 -118
- package/src/pages.js +128 -50
- package/src/partials.js +10 -6
- package/src/rss.js +4 -3
- package/src/server.js +90 -0
- package/src/sitemap.js +4 -1
- package/src/urls.js +116 -0
- package/src/watcher.js +16 -2
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const filePath = path.join(dirs.data, file);
|
|
38
|
+
const stat = await fs.stat(filePath);
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
// Skip directories
|
|
41
|
+
if (stat.isDirectory()) continue;
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const ext = path.extname(file).toLowerCase();
|
|
44
|
+
const name = path.basename(file, ext);
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
// Only process JSON and YAML files
|
|
47
|
+
if (!['.json', '.yaml', '.yml'].includes(ext)) continue;
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
try {
|
|
50
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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: "»"
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
6
|
+
const result = new CleanCSS({
|
|
7
|
+
level: 1,
|
|
8
|
+
rebase: false,
|
|
9
|
+
}).minify(css);
|
|
91
10
|
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
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
|
|
15
|
+
return result.styles;
|
|
100
16
|
};
|
|
101
17
|
|
|
102
|
-
const minifyJs = (js) => {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
27
|
+
return result.code || "";
|
|
112
28
|
};
|
|
113
29
|
|
|
114
|
-
const minifyHtml = (html) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 };
|