@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/MIGRATION.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Migrating to Swifty 4
|
|
2
|
+
|
|
3
|
+
Swifty 4 focuses on deterministic builds, safer deployment, and explicit errors.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Upgrade Node.js to version 22 or 24. Older Node.js releases are no longer supported.
|
|
8
|
+
- Run `npm install` after upgrading so the tracked lockfile installs the new minifiers.
|
|
9
|
+
|
|
10
|
+
## Clean Output and Public Files
|
|
11
|
+
|
|
12
|
+
Every full build empties the output directory. Files written directly to `dist/` are removed.
|
|
13
|
+
|
|
14
|
+
Move files that must pass through unchanged into `public/`:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
public/manifest.webmanifest -> dist/manifest.webmanifest
|
|
18
|
+
public/fonts/site.woff2 -> dist/fonts/site.woff2
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Optimized images remain cached in `.swifty-cache/`, which should not be committed.
|
|
22
|
+
|
|
23
|
+
## Folder Index Pages
|
|
24
|
+
|
|
25
|
+
`pages/blog/index.md` is now the content and front matter for `/blog`. It no longer creates `/blog/index` or gets treated as a child page.
|
|
26
|
+
|
|
27
|
+
## Permalinks and Base Paths
|
|
28
|
+
|
|
29
|
+
Use `permalink` in page front matter to override a page URL:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
permalink: /company/about.html
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For deployment below an origin path, set `base_path` and keep `site_url` as the origin:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
site_url: https://example.com
|
|
39
|
+
base_path: /project
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The base path changes public URLs but does not add a `dist/project/` directory.
|
|
43
|
+
|
|
44
|
+
## Strict Build Errors
|
|
45
|
+
|
|
46
|
+
Invalid configuration, front matter, JSON/YAML data, Eta templates, partial references, images, CSS, JavaScript, or generated HTML now fail the build. Fix the path reported in the error before deploying.
|
|
47
|
+
|
|
48
|
+
## Minification
|
|
49
|
+
|
|
50
|
+
Swifty now uses CleanCSS, Terser, and html-minifier-terser. JavaScript is compressed but identifiers are not mangled. Set `minify`, `minify_html`, `minify_css`, or `minify_js` to `false` if source formatting must be retained.
|
|
51
|
+
|
|
52
|
+
## Development Server
|
|
53
|
+
|
|
54
|
+
`swifty start` now uses Swifty's built-in static server. Set `server_port` in `config.yaml` to change its default port from `3000`.
|
|
55
|
+
|
|
56
|
+
## Deploy Command
|
|
57
|
+
|
|
58
|
+
`swifty deploy` stages only the configured output directory. It refuses to proceed when other changes are already staged, and it does not commit source changes automatically.
|
|
59
|
+
|
|
60
|
+
Commit source changes separately before deploying.
|
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ Swifty uses convention over configuration to make it super simple to build blazi
|
|
|
25
25
|
- **Previous/next navigation** - Auto-generated links between sibling pages
|
|
26
26
|
- **[Eta templating](https://eta.js.org/)** - Full JavaScript in templates with EJS syntax
|
|
27
27
|
- **Idiomorph navigation** with optional intent prefetching for SPA-like transitions
|
|
28
|
+
- **Custom permalinks and base paths** for flexible deployment URLs
|
|
29
|
+
- **Public asset passthrough** for files that should be copied unchanged
|
|
30
|
+
|
|
31
|
+
Requires Node.js 22 or newer. See [Migrating to Swifty 4](MIGRATION.md) when upgrading an existing site.
|
|
28
32
|
|
|
29
33
|
## Quickstart
|
|
30
34
|
|
|
@@ -48,6 +52,7 @@ your-site/
|
|
|
48
52
|
├── css/ # Stylesheets (auto-injected)
|
|
49
53
|
├── js/ # JavaScript (auto-injected)
|
|
50
54
|
├── images/ # Images (auto-optimized to responsive WebP)
|
|
55
|
+
├── public/ # Files copied unchanged to the output root
|
|
51
56
|
├── template.html # Base HTML template
|
|
52
57
|
└── config.yaml # Site configuration
|
|
53
58
|
```
|
|
@@ -59,14 +64,14 @@ npx swifty my-site # Create new site in my-site/ folder
|
|
|
59
64
|
npx swifty build # Build static site to dist/ (for production)
|
|
60
65
|
npx swifty start # Build, watch, and serve at localhost:3000 (for development)
|
|
61
66
|
npx swifty build --out dir # Build to custom output directory
|
|
62
|
-
npx swifty deploy "message" # Build,
|
|
67
|
+
npx swifty deploy "message" # Build, commit the output folder, and push
|
|
63
68
|
```
|
|
64
69
|
|
|
65
70
|
### Development vs Production
|
|
66
71
|
|
|
67
72
|
- **`swifty start`** - For development. Includes live reload (auto-refreshes browser on file changes) and file watching with incremental builds for CSS/JS/images.
|
|
68
73
|
- **`swifty build`** - For production deployment. Produces clean output without any development scripts.
|
|
69
|
-
- **`swifty deploy "message"`** - Builds the site and
|
|
74
|
+
- **`swifty deploy "message"`** - Builds the site, commits only the generated output folder, and pushes it to git.
|
|
70
75
|
|
|
71
76
|
## Documentation
|
|
72
77
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/swifty",
|
|
3
|
-
"version": "
|
|
4
|
-
"main": "index.js",
|
|
3
|
+
"version": "4.1.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": ">=22"
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
|
-
"src/"
|
|
16
|
+
"src/",
|
|
17
|
+
"MIGRATION.md"
|
|
11
18
|
],
|
|
12
19
|
"scripts": {
|
|
13
20
|
"test": "mocha",
|
|
21
|
+
"test:package": "node scripts/verify-package.js",
|
|
22
|
+
"check": "npm test && npm run test:package",
|
|
14
23
|
"build": "node src/build.js",
|
|
15
|
-
"start": "node src/
|
|
24
|
+
"start": "node src/cli.js start",
|
|
16
25
|
"init": "node src/init.js",
|
|
17
26
|
"watch": "node src/watcher.js"
|
|
18
27
|
},
|
|
@@ -26,16 +35,17 @@
|
|
|
26
35
|
"description": "Super Speedy Static Site Generator",
|
|
27
36
|
"dependencies": {
|
|
28
37
|
"chokidar": "^4.0.0",
|
|
38
|
+
"clean-css": "^5.3.3",
|
|
29
39
|
"eta": "^4.5.0",
|
|
30
40
|
"fs-extra": "^11.2.0",
|
|
31
|
-
"gray-matter": "^4.0.3",
|
|
32
41
|
"highlight.js": "^11.11.1",
|
|
42
|
+
"html-minifier-terser": "^7.2.0",
|
|
33
43
|
"js-yaml": "^4.1.0",
|
|
34
44
|
"livereload": "^0.10.3",
|
|
35
45
|
"marked": "^14.1.3",
|
|
36
46
|
"marked-highlight": "^2.2.1",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
47
|
+
"sharp": "^0.33.5",
|
|
48
|
+
"terser": "^5.48.0"
|
|
39
49
|
},
|
|
40
50
|
"devDependencies": {
|
|
41
51
|
"mocha": "^11.1.0"
|
|
@@ -45,10 +55,10 @@
|
|
|
45
55
|
},
|
|
46
56
|
"repository": {
|
|
47
57
|
"type": "git",
|
|
48
|
-
"url": "git+https://github.com/
|
|
58
|
+
"url": "git+https://github.com/daz-codes/swifty.git"
|
|
49
59
|
},
|
|
50
60
|
"bugs": {
|
|
51
|
-
"url": "https://github.com/
|
|
61
|
+
"url": "https://github.com/daz-codes/swifty/issues"
|
|
52
62
|
},
|
|
53
|
-
"homepage": "https://github.com/
|
|
63
|
+
"homepage": "https://github.com/daz-codes/swifty#readme"
|
|
54
64
|
}
|
package/src/assets.js
CHANGED
|
@@ -9,6 +9,11 @@ import sharp from "sharp";
|
|
|
9
9
|
import { dirs, defaultConfig } from "./config.js";
|
|
10
10
|
import { mapLimit } from "./concurrency.js";
|
|
11
11
|
import { minifyCss, minifyJs } from "./minify.js";
|
|
12
|
+
import {
|
|
13
|
+
applyBasePathToCss,
|
|
14
|
+
withBasePath,
|
|
15
|
+
withoutBasePath,
|
|
16
|
+
} from "./urls.js";
|
|
12
17
|
|
|
13
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
19
|
const __dirname = path.dirname(__filename);
|
|
@@ -33,7 +38,52 @@ const getBuildConcurrency = () => defaultConfig.build_concurrency || 16;
|
|
|
33
38
|
const navigationEnabled = () => defaultConfig.morphing !== false;
|
|
34
39
|
const minificationEnabled = (type) =>
|
|
35
40
|
defaultConfig.minify !== false && defaultConfig[`minify_${type}`] !== false;
|
|
36
|
-
const normalizeImageUrl = (url) =>
|
|
41
|
+
const normalizeImageUrl = (url) =>
|
|
42
|
+
withoutBasePath((url || "").split(/[?#]/)[0]);
|
|
43
|
+
|
|
44
|
+
const getImageCacheDirectory = () => {
|
|
45
|
+
const signature = crypto
|
|
46
|
+
.createHash("sha256")
|
|
47
|
+
.update(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
image_quality: defaultConfig.image_quality || 80,
|
|
50
|
+
max_image_width: defaultConfig.max_image_width || 800,
|
|
51
|
+
responsive_image_widths:
|
|
52
|
+
defaultConfig.responsive_image_widths || [320, 640, 800],
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
.digest("hex")
|
|
56
|
+
.slice(0, 12);
|
|
57
|
+
return path.join(dirs.cache, "images", signature);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const prepareImageCache = async () => {
|
|
61
|
+
const cacheRoot = path.join(dirs.cache, "images");
|
|
62
|
+
const cacheDirectory = getImageCacheDirectory();
|
|
63
|
+
await fsExtra.ensureDir(cacheDirectory);
|
|
64
|
+
|
|
65
|
+
const entries = await fs.readdir(cacheRoot, { withFileTypes: true });
|
|
66
|
+
await Promise.all(
|
|
67
|
+
entries
|
|
68
|
+
.filter(
|
|
69
|
+
(entry) =>
|
|
70
|
+
entry.isDirectory() &&
|
|
71
|
+
path.join(cacheRoot, entry.name) !== cacheDirectory,
|
|
72
|
+
)
|
|
73
|
+
.map((entry) => fsExtra.remove(path.join(cacheRoot, entry.name))),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return cacheDirectory;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const removeStaleCachedImages = async (cacheDirectory, expectedFiles) => {
|
|
80
|
+
const files = await fs.readdir(cacheDirectory);
|
|
81
|
+
await Promise.all(
|
|
82
|
+
files
|
|
83
|
+
.filter((file) => !expectedFiles.has(file))
|
|
84
|
+
.map((file) => fsExtra.remove(path.join(cacheDirectory, file))),
|
|
85
|
+
);
|
|
86
|
+
};
|
|
37
87
|
|
|
38
88
|
const getResponsiveWidths = (originalWidth) => {
|
|
39
89
|
const maxWidth = defaultConfig.max_image_width || 800;
|
|
@@ -62,7 +112,7 @@ const variantFilename = (filename, ext, width, outputWidth) =>
|
|
|
62
112
|
const registerResponsiveImage = (filename, ext, variants) => {
|
|
63
113
|
const basename = path.basename(filename, ext);
|
|
64
114
|
const entry = {
|
|
65
|
-
src: `/images/${basename}.webp
|
|
115
|
+
src: withBasePath(`/images/${basename}.webp`),
|
|
66
116
|
srcset: variants
|
|
67
117
|
.map((variant) => `${variant.url} ${variant.width}w`)
|
|
68
118
|
.join(", "),
|
|
@@ -70,7 +120,7 @@ const registerResponsiveImage = (filename, ext, variants) => {
|
|
|
70
120
|
variants,
|
|
71
121
|
};
|
|
72
122
|
|
|
73
|
-
responsiveImageMap.set(normalizeImageUrl(`/images/${filename}`), entry);
|
|
123
|
+
responsiveImageMap.set(normalizeImageUrl(withBasePath(`/images/${filename}`)), entry);
|
|
74
124
|
responsiveImageMap.set(normalizeImageUrl(entry.src), entry);
|
|
75
125
|
};
|
|
76
126
|
|
|
@@ -91,7 +141,7 @@ const getNavigationAssetName = async (filename = "swifty-navigation.js") => {
|
|
|
91
141
|
|
|
92
142
|
const getNavigationScriptSrc = async () => {
|
|
93
143
|
if (!navigationEnabled()) return "";
|
|
94
|
-
return `/swifty/${await getNavigationAssetName()}
|
|
144
|
+
return withBasePath(`/swifty/${await getNavigationAssetName()}`);
|
|
95
145
|
};
|
|
96
146
|
|
|
97
147
|
const isDestinationFresh = async (source, destination) => {
|
|
@@ -115,22 +165,37 @@ const copyIfStale = async (source, destination) => {
|
|
|
115
165
|
return true;
|
|
116
166
|
};
|
|
117
167
|
|
|
118
|
-
const optimizeImageVariantToWebp = async (
|
|
119
|
-
|
|
120
|
-
|
|
168
|
+
const optimizeImageVariantToWebp = async (
|
|
169
|
+
source,
|
|
170
|
+
destination,
|
|
171
|
+
cacheDestination,
|
|
172
|
+
width,
|
|
173
|
+
) => {
|
|
174
|
+
let optimized = false;
|
|
175
|
+
|
|
176
|
+
if (!(await isDestinationFresh(source, cacheDestination))) {
|
|
177
|
+
const imageQuality = defaultConfig.image_quality || 80;
|
|
178
|
+
await fsExtra.ensureDir(path.dirname(cacheDestination));
|
|
179
|
+
|
|
180
|
+
await sharp(source)
|
|
181
|
+
.resize({ width })
|
|
182
|
+
.toFormat("webp", { quality: imageQuality })
|
|
183
|
+
.toFile(cacheDestination);
|
|
184
|
+
optimized = true;
|
|
121
185
|
}
|
|
122
186
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
await sharp(source)
|
|
126
|
-
.resize({ width })
|
|
127
|
-
.toFormat("webp", { quality: imageQuality })
|
|
128
|
-
.toFile(destination);
|
|
129
|
-
|
|
130
|
-
return true;
|
|
187
|
+
await copyIfStale(cacheDestination, destination);
|
|
188
|
+
return optimized;
|
|
131
189
|
};
|
|
132
190
|
|
|
133
|
-
const optimizeImageToWebp = async (
|
|
191
|
+
const optimizeImageToWebp = async (
|
|
192
|
+
source,
|
|
193
|
+
imagesFolder,
|
|
194
|
+
cacheDirectory,
|
|
195
|
+
filename,
|
|
196
|
+
ext,
|
|
197
|
+
expectedCacheFiles,
|
|
198
|
+
) => {
|
|
134
199
|
const metadata = await sharp(source).metadata();
|
|
135
200
|
const originalWidth = metadata.width || 0;
|
|
136
201
|
const maxWidth = defaultConfig.max_image_width || 800;
|
|
@@ -138,17 +203,24 @@ const optimizeImageToWebp = async (source, imagesFolder, filename, ext) => {
|
|
|
138
203
|
originalWidth > 0 ? Math.min(originalWidth, maxWidth) : maxWidth;
|
|
139
204
|
const variants = getResponsiveWidths(originalWidth).map((width) => {
|
|
140
205
|
const file = variantFilename(filename, ext, width, outputWidth);
|
|
206
|
+
expectedCacheFiles?.add(file);
|
|
141
207
|
return {
|
|
142
208
|
width,
|
|
143
209
|
file,
|
|
144
|
-
url: `/images/${file}
|
|
210
|
+
url: withBasePath(`/images/${file}`),
|
|
145
211
|
destination: path.join(imagesFolder, file),
|
|
212
|
+
cacheDestination: path.join(cacheDirectory, file),
|
|
146
213
|
};
|
|
147
214
|
});
|
|
148
215
|
|
|
149
216
|
const results = await Promise.all(
|
|
150
217
|
variants.map((variant) =>
|
|
151
|
-
optimizeImageVariantToWebp(
|
|
218
|
+
optimizeImageVariantToWebp(
|
|
219
|
+
source,
|
|
220
|
+
variant.destination,
|
|
221
|
+
variant.cacheDestination,
|
|
222
|
+
variant.width,
|
|
223
|
+
),
|
|
152
224
|
),
|
|
153
225
|
);
|
|
154
226
|
|
|
@@ -163,12 +235,21 @@ const optimizeImageToWebp = async (source, imagesFolder, filename, ext) => {
|
|
|
163
235
|
|
|
164
236
|
const processTextAsset = async (source, destination, type) => {
|
|
165
237
|
const content = await fs.readFile(source, "utf-8");
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
238
|
+
let nextContent = content;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (type === "css" && minificationEnabled("css")) {
|
|
242
|
+
nextContent = minifyCss(applyBasePathToCss(content));
|
|
243
|
+
} else if (type === "js" && minificationEnabled("js")) {
|
|
244
|
+
nextContent = await minifyJs(content);
|
|
245
|
+
} else if (type === "css") {
|
|
246
|
+
nextContent = applyBasePathToCss(content);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new Error(`Unable to minify ${source}: ${error.message}`, {
|
|
250
|
+
cause: error,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
172
253
|
|
|
173
254
|
try {
|
|
174
255
|
const currentContent = await fs.readFile(destination, "utf-8");
|
|
@@ -236,6 +317,9 @@ const copyNavigationAssets = async (outputDir = dirs.dist) => {
|
|
|
236
317
|
};
|
|
237
318
|
|
|
238
319
|
const copyAssets = async (outputDir = dirs.dist) => {
|
|
320
|
+
if (await fsExtra.pathExists(dirs.public)) {
|
|
321
|
+
await fsExtra.copy(dirs.public, outputDir, { overwrite: true });
|
|
322
|
+
}
|
|
239
323
|
await ensureAndCopy(
|
|
240
324
|
dirs.css,
|
|
241
325
|
path.join(outputDir, "css"),
|
|
@@ -249,11 +333,14 @@ async function optimizeImages(outputDir = dirs.dist) {
|
|
|
249
333
|
|
|
250
334
|
try {
|
|
251
335
|
if (!(await fsExtra.pathExists(dirs.images))) {
|
|
336
|
+
await fsExtra.remove(path.join(dirs.cache, "images"));
|
|
252
337
|
console.log(`No ${path.basename(dirs.images)} found in ${dirs.images}`);
|
|
253
338
|
return;
|
|
254
339
|
}
|
|
255
340
|
|
|
256
341
|
const imagesFolder = path.join(outputDir, "images");
|
|
342
|
+
const cacheDirectory = await prepareImageCache();
|
|
343
|
+
const expectedCacheFiles = new Set();
|
|
257
344
|
await fsExtra.ensureDir(imagesFolder);
|
|
258
345
|
|
|
259
346
|
const files = await fs.readdir(dirs.images);
|
|
@@ -278,8 +365,10 @@ async function optimizeImages(outputDir = dirs.dist) {
|
|
|
278
365
|
const optimized = await optimizeImageToWebp(
|
|
279
366
|
filePath,
|
|
280
367
|
imagesFolder,
|
|
368
|
+
cacheDirectory,
|
|
281
369
|
file,
|
|
282
370
|
ext,
|
|
371
|
+
expectedCacheFiles,
|
|
283
372
|
);
|
|
284
373
|
if (optimized) {
|
|
285
374
|
console.log(`Optimized ${file}`);
|
|
@@ -287,8 +376,11 @@ async function optimizeImages(outputDir = dirs.dist) {
|
|
|
287
376
|
},
|
|
288
377
|
getBuildConcurrency(),
|
|
289
378
|
);
|
|
379
|
+
await removeStaleCachedImages(cacheDirectory, expectedCacheFiles);
|
|
290
380
|
} catch (error) {
|
|
291
|
-
|
|
381
|
+
throw new Error(`Unable to optimize images: ${error.message}`, {
|
|
382
|
+
cause: error,
|
|
383
|
+
});
|
|
292
384
|
}
|
|
293
385
|
}
|
|
294
386
|
const generateAssetImports = async (dir, tagTemplate, validExts) => {
|
|
@@ -309,25 +401,25 @@ const generateAssetImports = async (dir, tagTemplate, validExts) => {
|
|
|
309
401
|
const getCssImports = () =>
|
|
310
402
|
generateAssetImports(
|
|
311
403
|
dirs.css,
|
|
312
|
-
(file, mtime) => `<link rel="stylesheet" href="
|
|
404
|
+
(file, mtime) => `<link rel="stylesheet" href="${withBasePath(`/css/${file}`)}?v=${mtime}" />`,
|
|
313
405
|
validExtensions.css,
|
|
314
406
|
);
|
|
315
407
|
const getJsImports = () =>
|
|
316
408
|
generateAssetImports(
|
|
317
409
|
dirs.js,
|
|
318
|
-
(file, mtime) => `<script src="
|
|
410
|
+
(file, mtime) => `<script src="${withBasePath(`/js/${file}`)}?v=${mtime}"></script>`,
|
|
319
411
|
validExtensions.js,
|
|
320
412
|
);
|
|
321
413
|
const getCssPreloads = () =>
|
|
322
414
|
generateAssetImports(
|
|
323
415
|
dirs.css,
|
|
324
|
-
(file, mtime) => `<link rel="preload" href="
|
|
416
|
+
(file, mtime) => `<link rel="preload" href="${withBasePath(`/css/${file}`)}?v=${mtime}" as="style" />`,
|
|
325
417
|
validExtensions.css,
|
|
326
418
|
);
|
|
327
419
|
const getJsPreloads = () =>
|
|
328
420
|
generateAssetImports(
|
|
329
421
|
dirs.js,
|
|
330
|
-
(file, mtime) => `<link rel="preload" href="
|
|
422
|
+
(file, mtime) => `<link rel="preload" href="${withBasePath(`/js/${file}`)}?v=${mtime}" as="script" />`,
|
|
331
423
|
validExtensions.js,
|
|
332
424
|
);
|
|
333
425
|
|
|
@@ -359,6 +451,7 @@ const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
|
|
|
359
451
|
const filename = path.basename(filePath);
|
|
360
452
|
const ext = path.extname(filePath).toLowerCase();
|
|
361
453
|
const imagesFolder = path.join(outputDir, "images");
|
|
454
|
+
const cacheDirectory = await prepareImageCache();
|
|
362
455
|
|
|
363
456
|
await fsExtra.ensureDir(imagesFolder);
|
|
364
457
|
|
|
@@ -374,6 +467,7 @@ const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
|
|
|
374
467
|
const optimized = await optimizeImageToWebp(
|
|
375
468
|
filePath,
|
|
376
469
|
imagesFolder,
|
|
470
|
+
cacheDirectory,
|
|
377
471
|
filename,
|
|
378
472
|
ext,
|
|
379
473
|
);
|
|
@@ -394,4 +488,5 @@ export {
|
|
|
394
488
|
optimizeSingleImage,
|
|
395
489
|
getNavigationScriptSrc,
|
|
396
490
|
getResponsiveImage,
|
|
491
|
+
getImageCacheDirectory,
|
|
397
492
|
};
|
package/src/build.js
CHANGED
|
@@ -1,22 +1,54 @@
|
|
|
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
|
+
const sourceDirectories = Object.entries(dirs)
|
|
22
|
+
.filter(([name]) => name !== "dist")
|
|
23
|
+
.map(([, directory]) => path.resolve(directory));
|
|
24
|
+
const outputOverlapsSource = sourceDirectories.some(
|
|
25
|
+
(directory) =>
|
|
26
|
+
outputPath === directory || outputPath.startsWith(`${directory}${path.sep}`),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (outputContainsProject || outputOverlapsSource) {
|
|
30
|
+
throw new Error(`Refusing to empty unsafe output directory: ${outputPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await fsExtra.emptyDir(outputPath);
|
|
34
|
+
return outputPath;
|
|
35
|
+
};
|
|
8
36
|
|
|
9
37
|
export default async function build(outputDir = dirs.dist) {
|
|
10
38
|
const startTime = performance.now();
|
|
11
39
|
console.log("🚀 Starting build...");
|
|
40
|
+
const resolvedOutputDir = await prepareOutputDirectory(outputDir);
|
|
41
|
+
clearDataCache();
|
|
42
|
+
clearPartialCache();
|
|
12
43
|
|
|
13
44
|
const copyStart = performance.now();
|
|
14
|
-
await copyAssets(
|
|
45
|
+
await copyAssets(resolvedOutputDir);
|
|
15
46
|
const copyTime = performance.now() - copyStart;
|
|
16
47
|
console.log(`📁 Assets copied in ${(copyTime / 1000).toFixed(2)}s`);
|
|
48
|
+
await resetCaches();
|
|
17
49
|
|
|
18
50
|
const imageStart = performance.now();
|
|
19
|
-
await optimizeImages(
|
|
51
|
+
await optimizeImages(resolvedOutputDir);
|
|
20
52
|
const imageTime = performance.now() - imageStart;
|
|
21
53
|
console.log(`🖼️ Images optimized in ${(imageTime / 1000).toFixed(2)}s`);
|
|
22
54
|
|
|
@@ -31,17 +63,17 @@ export default async function build(outputDir = dirs.dist) {
|
|
|
31
63
|
console.log(`🔗 Links added in ${(linksTime / 1000).toFixed(2)}s`);
|
|
32
64
|
|
|
33
65
|
const createStart = performance.now();
|
|
34
|
-
await createPages(pages,
|
|
66
|
+
await createPages(pages, resolvedOutputDir);
|
|
35
67
|
const createTime = performance.now() - createStart;
|
|
36
68
|
console.log(`✨ Pages created in ${(createTime / 1000).toFixed(2)}s`);
|
|
37
69
|
|
|
38
70
|
const rssStart = performance.now();
|
|
39
|
-
await generateRssFeeds(pages,
|
|
71
|
+
await generateRssFeeds(pages, resolvedOutputDir);
|
|
40
72
|
const rssTime = performance.now() - rssStart;
|
|
41
73
|
console.log(`📡 RSS feeds generated in ${(rssTime / 1000).toFixed(2)}s`);
|
|
42
74
|
|
|
43
75
|
const seoStart = performance.now();
|
|
44
|
-
await generateSeoFiles(pages,
|
|
76
|
+
await generateSeoFiles(pages, resolvedOutputDir);
|
|
45
77
|
const seoTime = performance.now() - seoStart;
|
|
46
78
|
console.log(`🧭 SEO files generated in ${(seoTime / 1000).toFixed(2)}s`);
|
|
47
79
|
|
|
@@ -70,6 +102,8 @@ export default async function build(outputDir = dirs.dist) {
|
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
|
|
105
|
+
export { prepareOutputDirectory };
|
|
106
|
+
|
|
73
107
|
// Run the build when invoked directly (e.g. `npm run build`, `npm start`),
|
|
74
108
|
// not when imported as a module (e.g. by cli.js).
|
|
75
109
|
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 {
|
|
3
|
+
import { execFileSync, 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
|
|
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];
|
|
@@ -52,8 +92,8 @@ async function main() {
|
|
|
52
92
|
watcher.default(outDir);
|
|
53
93
|
}
|
|
54
94
|
|
|
55
|
-
|
|
56
|
-
|
|
95
|
+
const server = await import("./server.js");
|
|
96
|
+
await server.default(outDir);
|
|
57
97
|
break;
|
|
58
98
|
}
|
|
59
99
|
case "watch": {
|
|
@@ -64,7 +104,11 @@ async function main() {
|
|
|
64
104
|
break;
|
|
65
105
|
}
|
|
66
106
|
case "deploy": {
|
|
67
|
-
const
|
|
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
|
-
|
|
78
|
-
|
|
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,11 @@ async function main() {
|
|
|
104
138
|
}
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
|
|
141
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
142
|
+
main().catch((error) => {
|
|
143
|
+
console.error(`Swifty failed: ${error.message}`);
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { commitAndPushOutput, main };
|