@inglorious/ssx 1.0.0 → 1.1.1
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 +140 -61
- package/bin/ssx.js +17 -21
- package/package.json +3 -5
- package/src/build/build.test.js +124 -0
- package/src/build/index.js +178 -0
- package/src/build/manifest.js +120 -0
- package/src/build/manifest.test.js +153 -0
- package/src/build/metadata.js +53 -0
- package/src/build/pages.js +52 -0
- package/src/build/pages.test.js +83 -0
- package/src/build/public.js +49 -0
- package/src/build/public.test.js +59 -0
- package/src/build/rss.js +121 -0
- package/src/build/rss.test.js +104 -0
- package/src/build/sitemap.js +66 -0
- package/src/build/sitemap.test.js +84 -0
- package/src/build/vite-config.js +51 -0
- package/src/dev/index.js +98 -0
- package/src/dev/vite-config.js +60 -0
- package/src/dev/vite-config.test.js +46 -0
- package/src/{html.js → render/html.js} +33 -23
- package/src/render/index.js +53 -0
- package/src/render/layout.js +52 -0
- package/src/render/layout.test.js +58 -0
- package/src/render/render.test.js +114 -0
- package/src/router/index.js +293 -0
- package/src/{router.test.js → router/router.test.js} +35 -4
- package/src/scripts/app.js +15 -5
- package/src/scripts/app.test.js +49 -30
- package/src/{store.js → store/index.js} +11 -1
- package/src/store/store.test.js +56 -0
- package/src/utils/config.js +16 -0
- package/src/{module.js → utils/module.js} +8 -3
- package/src/utils/module.test.js +64 -0
- package/src/utils/page-options.js +17 -0
- package/src/utils/page-options.test.js +57 -0
- package/src/build.js +0 -96
- package/src/build.test.js +0 -11
- package/src/dev.js +0 -111
- package/src/module.test.js +0 -45
- package/src/random.js +0 -30
- package/src/render.js +0 -48
- package/src/render.test.js +0 -72
- package/src/router.js +0 -231
- package/src/store.test.js +0 -40
- package/src/vite-config.js +0 -40
- /package/src/{html.test.js → render/html.test.js} +0 -0
package/README.md
CHANGED
|
@@ -77,7 +77,13 @@ export const index = {
|
|
|
77
77
|
},
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export const
|
|
80
|
+
export const metadata = {
|
|
81
|
+
title: "Home",
|
|
82
|
+
meta: {
|
|
83
|
+
description: "Welcome to our site",
|
|
84
|
+
"og:image": "/og-image.png",
|
|
85
|
+
},
|
|
86
|
+
}
|
|
81
87
|
```
|
|
82
88
|
|
|
83
89
|
### Development
|
|
@@ -113,7 +119,47 @@ Deploy `dist/` to:
|
|
|
113
119
|
|
|
114
120
|
## Features
|
|
115
121
|
|
|
116
|
-
###
|
|
122
|
+
### �️ Sitemap & RSS Generation
|
|
123
|
+
|
|
124
|
+
SSX automatically generates `sitemap.xml` and `rss.xml` based on your pages. Configure them in `src/site.config.js`:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
export default {
|
|
128
|
+
// Basic metadata
|
|
129
|
+
title: "My Awesome Site",
|
|
130
|
+
meta: {
|
|
131
|
+
description: "A site built with SSX",
|
|
132
|
+
"og:type": "website",
|
|
133
|
+
"og:site_name": "My Site",
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Sitemap configuration
|
|
137
|
+
sitemap: {
|
|
138
|
+
hostname: "https://myblog.com",
|
|
139
|
+
filter: (page) => !["/admin", "/draft-*", "/test"].includes(page.pattern),
|
|
140
|
+
defaults: {
|
|
141
|
+
changefreq: "weekly",
|
|
142
|
+
priority: 0.5,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// RSS configuration
|
|
147
|
+
rss: {
|
|
148
|
+
title: "My Blog",
|
|
149
|
+
description: "Latest posts from my blog",
|
|
150
|
+
link: "https://myblog.com",
|
|
151
|
+
feedPath: "/feed.xml",
|
|
152
|
+
language: "en",
|
|
153
|
+
copyright: "© 2026 My Blog",
|
|
154
|
+
maxItems: 10,
|
|
155
|
+
filter: (page) => page.path.startsWith("/posts/"),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Pages with a `published` date in metadata are included in RSS feeds.
|
|
161
|
+
|
|
162
|
+
### �📁 File-Based Routing
|
|
117
163
|
|
|
118
164
|
Your file structure defines your routes:
|
|
119
165
|
|
|
@@ -196,7 +242,7 @@ export const title = "Blog"
|
|
|
196
242
|
|
|
197
243
|
The `load` function runs on the server during build. Data is serialized into the HTML and available immediately on the client.
|
|
198
244
|
|
|
199
|
-
### 🎨 Dynamic Routes with `
|
|
245
|
+
### 🎨 Dynamic Routes with `staticPaths`
|
|
200
246
|
|
|
201
247
|
Generate multiple pages from data:
|
|
202
248
|
|
|
@@ -224,22 +270,27 @@ export async function load(entity, page) {
|
|
|
224
270
|
}
|
|
225
271
|
|
|
226
272
|
// Tell SSX which pages to generate
|
|
227
|
-
export async function
|
|
273
|
+
export async function staticPaths() {
|
|
228
274
|
const response = await fetch(`https://api.example.com/posts`)
|
|
229
275
|
const posts = await response.json()
|
|
230
276
|
|
|
231
277
|
return posts.map((post) => ({
|
|
232
|
-
params: {
|
|
233
|
-
path: `/posts/${post.
|
|
278
|
+
params: { slug: post.slug },
|
|
279
|
+
path: `/posts/${post.slug}`,
|
|
234
280
|
}))
|
|
235
281
|
}
|
|
236
282
|
|
|
237
|
-
export const
|
|
283
|
+
export const metadata = (entity) => ({
|
|
284
|
+
title: entity.post.title ?? "Post",
|
|
285
|
+
meta: {
|
|
286
|
+
description: entity.post.excerpt,
|
|
287
|
+
},
|
|
288
|
+
})
|
|
238
289
|
```
|
|
239
290
|
|
|
240
291
|
### 📄 Page Metadata
|
|
241
292
|
|
|
242
|
-
Export metadata for HTML `<head
|
|
293
|
+
Export metadata for HTML `<head>`. The `metadata` export can be a plain object or a function:
|
|
243
294
|
|
|
244
295
|
```javascript
|
|
245
296
|
export const index = {
|
|
@@ -249,22 +300,22 @@ export const index = {
|
|
|
249
300
|
}
|
|
250
301
|
|
|
251
302
|
// Static metadata
|
|
252
|
-
export const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
303
|
+
export const metadata = {
|
|
304
|
+
title: "My Site",
|
|
305
|
+
meta: {
|
|
306
|
+
description: "An awesome static site",
|
|
307
|
+
"og:image": "/og-image.png",
|
|
308
|
+
},
|
|
256
309
|
}
|
|
257
310
|
|
|
258
311
|
// Or dynamic metadata (uses entity data)
|
|
259
|
-
export const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
312
|
+
export const metadata = (entity) => ({
|
|
313
|
+
title: `${entity.user.name}'s Profile`,
|
|
314
|
+
meta: {
|
|
315
|
+
description: entity.user.bio,
|
|
316
|
+
"og:image": entity.user.avatar,
|
|
317
|
+
},
|
|
263
318
|
})
|
|
264
|
-
|
|
265
|
-
// Include CSS/JS files
|
|
266
|
-
export const styles = ["./home.css", "./theme.css"]
|
|
267
|
-
export const scripts = ["./analytics.js"]
|
|
268
319
|
```
|
|
269
320
|
|
|
270
321
|
### 🔥 Client-Side Hydration
|
|
@@ -326,11 +377,11 @@ Builds your static site:
|
|
|
326
377
|
ssx build [options]
|
|
327
378
|
|
|
328
379
|
Options:
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
-
|
|
332
|
-
--
|
|
333
|
-
--
|
|
380
|
+
-c, --config <file> Config file (default: "site.config.js")
|
|
381
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
382
|
+
-o, --out <dir> Output directory (default: "dist")
|
|
383
|
+
-i, --incremental Enable incremental builds (default: true)
|
|
384
|
+
-f, --force Force clean build, ignore cache
|
|
334
385
|
```
|
|
335
386
|
|
|
336
387
|
### `ssx dev`
|
|
@@ -341,8 +392,9 @@ Starts development server with hot reload:
|
|
|
341
392
|
ssx dev [options]
|
|
342
393
|
|
|
343
394
|
Options:
|
|
344
|
-
-
|
|
345
|
-
-
|
|
395
|
+
-c, --config <file> Config file (default: "site.config.js")
|
|
396
|
+
-r, --root <dir> Source root directory (default: "src")
|
|
397
|
+
-p, --port <port> Dev server port (default: 3000)
|
|
346
398
|
```
|
|
347
399
|
|
|
348
400
|
### Package.json Scripts
|
|
@@ -352,7 +404,7 @@ Options:
|
|
|
352
404
|
"scripts": {
|
|
353
405
|
"dev": "ssx dev",
|
|
354
406
|
"build": "ssx build",
|
|
355
|
-
"preview": "
|
|
407
|
+
"preview": "pnpm dlx serve dist"
|
|
356
408
|
}
|
|
357
409
|
}
|
|
358
410
|
```
|
|
@@ -374,7 +426,7 @@ my-site/
|
|
|
374
426
|
│ └── types/ # Custom entity types (optional)
|
|
375
427
|
├── dist/ # Build output
|
|
376
428
|
├── package.json
|
|
377
|
-
└──
|
|
429
|
+
└── site.config.js # Site configuration
|
|
378
430
|
```
|
|
379
431
|
|
|
380
432
|
---
|
|
@@ -404,23 +456,52 @@ SSX is perfect if you:
|
|
|
404
456
|
|
|
405
457
|
## Advanced Usage
|
|
406
458
|
|
|
407
|
-
###
|
|
459
|
+
### Site Configuration
|
|
408
460
|
|
|
409
|
-
|
|
461
|
+
Customize SSX behavior in `src/site.config.js`:
|
|
410
462
|
|
|
411
463
|
```javascript
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
464
|
+
export default {
|
|
465
|
+
// Basic metadata
|
|
466
|
+
lang: "en",
|
|
467
|
+
charset: "UTF-8",
|
|
468
|
+
title: "My Awesome Site",
|
|
469
|
+
meta: {
|
|
470
|
+
description: "A site built with SSX",
|
|
471
|
+
"og:type": "website",
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
// Global assets
|
|
475
|
+
styles: ["./styles/reset.css", "./styles/theme.css"],
|
|
476
|
+
scripts: ["./scripts/analytics.js"],
|
|
477
|
+
|
|
478
|
+
// Build options
|
|
479
|
+
basePath: "/",
|
|
480
|
+
rootDir: "src",
|
|
481
|
+
outDir: "dist",
|
|
482
|
+
publicDir: "public",
|
|
483
|
+
favicon: "/favicon.ico",
|
|
484
|
+
|
|
485
|
+
// Router config
|
|
486
|
+
router: {
|
|
487
|
+
trailingSlash: false,
|
|
488
|
+
scrollBehavior: "smooth",
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
// Vite config passthrough
|
|
492
|
+
vite: {
|
|
493
|
+
server: {
|
|
494
|
+
port: 3000,
|
|
495
|
+
open: true,
|
|
421
496
|
},
|
|
422
497
|
},
|
|
423
|
-
|
|
498
|
+
|
|
499
|
+
// Build hooks
|
|
500
|
+
hooks: {
|
|
501
|
+
beforeBuild: async (config) => console.log("Starting build..."),
|
|
502
|
+
afterBuild: async (result) => console.log(`Built ${result.pages} pages`),
|
|
503
|
+
},
|
|
504
|
+
}
|
|
424
505
|
```
|
|
425
506
|
|
|
426
507
|
### Environment Variables
|
|
@@ -452,7 +533,9 @@ export const notFound = {
|
|
|
452
533
|
},
|
|
453
534
|
}
|
|
454
535
|
|
|
455
|
-
export const
|
|
536
|
+
export const metadata = {
|
|
537
|
+
title: "404",
|
|
538
|
+
}
|
|
456
539
|
```
|
|
457
540
|
|
|
458
541
|
Register it in your router:
|
|
@@ -469,13 +552,17 @@ setRoutes({
|
|
|
469
552
|
|
|
470
553
|
### Incremental Builds
|
|
471
554
|
|
|
472
|
-
|
|
555
|
+
SSX enables incremental builds by default. Only changed pages are rebuilt, dramatically speeding up your build process:
|
|
473
556
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
557
|
+
```bash
|
|
558
|
+
ssx build
|
|
559
|
+
# Only changed pages are rebuilt
|
|
477
560
|
|
|
478
|
-
|
|
561
|
+
ssx build --force
|
|
562
|
+
# Force a clean rebuild of all pages
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Incremental builds respect your page dependencies and invalidate cache when dependencies change.
|
|
479
566
|
|
|
480
567
|
---
|
|
481
568
|
|
|
@@ -487,14 +574,11 @@ Incremental builds are planned for future releases.
|
|
|
487
574
|
import { build } from "@inglorious/ssx/build"
|
|
488
575
|
|
|
489
576
|
await build({
|
|
490
|
-
rootDir: "src",
|
|
491
|
-
outDir: "dist",
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
styles: [], // Global CSS files
|
|
496
|
-
scripts: [], // Global JS files
|
|
497
|
-
},
|
|
577
|
+
rootDir: "src",
|
|
578
|
+
outDir: "dist",
|
|
579
|
+
configFile: "site.config.js",
|
|
580
|
+
incremental: true,
|
|
581
|
+
clean: false,
|
|
498
582
|
})
|
|
499
583
|
```
|
|
500
584
|
|
|
@@ -506,9 +590,7 @@ import { dev } from "@inglorious/ssx/dev"
|
|
|
506
590
|
await dev({
|
|
507
591
|
rootDir: "src",
|
|
508
592
|
port: 3000,
|
|
509
|
-
|
|
510
|
-
// ... same as build
|
|
511
|
-
},
|
|
593
|
+
configFile: "site.config.js",
|
|
512
594
|
})
|
|
513
595
|
```
|
|
514
596
|
|
|
@@ -529,10 +611,7 @@ Check out these example projects:
|
|
|
529
611
|
|
|
530
612
|
- [ ] TypeScript support
|
|
531
613
|
- [ ] Image optimization
|
|
532
|
-
- [ ] Incremental builds
|
|
533
614
|
- [ ] API routes (serverless functions)
|
|
534
|
-
- [ ] RSS feed generation
|
|
535
|
-
- [ ] Sitemap generation
|
|
536
615
|
- [ ] MDX support
|
|
537
616
|
- [ ] i18n helpers
|
|
538
617
|
|
package/bin/ssx.js
CHANGED
|
@@ -5,8 +5,8 @@ import { fileURLToPath } from "node:url"
|
|
|
5
5
|
|
|
6
6
|
import { Command } from "commander"
|
|
7
7
|
|
|
8
|
-
import { build } from "../src/build.js"
|
|
9
|
-
import { dev } from "../src/dev.js"
|
|
8
|
+
import { build } from "../src/build/index.js"
|
|
9
|
+
import { dev } from "../src/dev/index.js"
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url)
|
|
12
12
|
const __dirname = path.dirname(__filename)
|
|
@@ -26,24 +26,17 @@ program
|
|
|
26
26
|
program
|
|
27
27
|
.command("dev")
|
|
28
28
|
.description("Start development server with hot reload")
|
|
29
|
+
.option("-c, --config <file>", "config file", "site.config.js")
|
|
29
30
|
.option("-r, --root <dir>", "source root directory", "src")
|
|
30
31
|
.option("-p, --port <port>", "dev server port", 3000)
|
|
31
|
-
.option("-t, --title <title>", "default page title", "My Site")
|
|
32
|
-
.option("--styles <styles...>", "CSS files to include")
|
|
33
|
-
.option("--scripts <scripts...>", "JS files to include")
|
|
34
32
|
.action(async (options) => {
|
|
35
33
|
const cwd = process.cwd()
|
|
36
34
|
|
|
37
35
|
try {
|
|
38
36
|
await dev({
|
|
37
|
+
...options,
|
|
39
38
|
rootDir: path.resolve(cwd, options.root),
|
|
40
39
|
port: Number(options.port),
|
|
41
|
-
renderOptions: {
|
|
42
|
-
title: options.title,
|
|
43
|
-
meta: {},
|
|
44
|
-
styles: options.styles || [],
|
|
45
|
-
scripts: options.scripts || [],
|
|
46
|
-
},
|
|
47
40
|
})
|
|
48
41
|
} catch (error) {
|
|
49
42
|
console.error("Dev server failed:", error)
|
|
@@ -53,26 +46,29 @@ program
|
|
|
53
46
|
|
|
54
47
|
program
|
|
55
48
|
.command("build")
|
|
56
|
-
.description("Build
|
|
49
|
+
.description("Build site from pages directory")
|
|
50
|
+
.option("-c, --config <file>", "config file", "site.config.js")
|
|
57
51
|
.option("-r, --root <dir>", "source root directory", "src")
|
|
58
52
|
.option("-o, --out <dir>", "output directory", "dist")
|
|
59
|
-
.option("-
|
|
60
|
-
.option("--
|
|
61
|
-
.option("--scripts <scripts...>", "JS files to include")
|
|
53
|
+
.option("-i, --incremental", "enable incremental builds", true)
|
|
54
|
+
.option("-f, --force", "force clean build (ignore cache)", false)
|
|
62
55
|
.action(async (options) => {
|
|
63
56
|
const cwd = process.cwd()
|
|
64
57
|
|
|
65
58
|
try {
|
|
66
59
|
await build({
|
|
60
|
+
...options,
|
|
67
61
|
rootDir: path.resolve(cwd, options.root),
|
|
68
62
|
outDir: path.resolve(cwd, options.out),
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
meta: {},
|
|
72
|
-
styles: options.styles || [],
|
|
73
|
-
scripts: options.scripts || [],
|
|
74
|
-
},
|
|
63
|
+
incremental: options.incremental, // Enabled by default
|
|
64
|
+
clean: options.force,
|
|
75
65
|
})
|
|
66
|
+
|
|
67
|
+
// if (result.skipped) {
|
|
68
|
+
// console.log(
|
|
69
|
+
// `\n⚡ Incremental build saved time by skipping ${result.skipped} unchanged pages`,
|
|
70
|
+
// )
|
|
71
|
+
// }
|
|
76
72
|
} catch (error) {
|
|
77
73
|
console.error("Build failed:", error)
|
|
78
74
|
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ssx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Server-Side-X. Xecution? Xperience? Who knows.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,10 +26,7 @@
|
|
|
26
26
|
"ssx": "./bin/ssx.js"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
|
-
"./
|
|
30
|
-
"./router": "./src/router.js",
|
|
31
|
-
"./render": "./src/render.js",
|
|
32
|
-
"./html": "./src/html.js"
|
|
29
|
+
"./site.config": "./types/site.config.d.ts"
|
|
33
30
|
},
|
|
34
31
|
"files": [
|
|
35
32
|
"bin",
|
|
@@ -44,6 +41,7 @@
|
|
|
44
41
|
"@lit-labs/ssr": "^4.0.0",
|
|
45
42
|
"commander": "^14.0.2",
|
|
46
43
|
"connect": "^3.7.0",
|
|
44
|
+
"fast-xml-parser": "^5.3.3",
|
|
47
45
|
"glob": "^13.0.0",
|
|
48
46
|
"rollup-plugin-minify-template-literals": "^1.1.7",
|
|
49
47
|
"vite": "^7.1.3",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { build as viteBuild } from "vite"
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
6
|
+
|
|
7
|
+
import { getPages } from "../router/index.js"
|
|
8
|
+
import { generateApp } from "../scripts/app.js"
|
|
9
|
+
import { generateStore } from "../store/index.js"
|
|
10
|
+
import { loadConfig } from "../utils/config.js"
|
|
11
|
+
import { build } from "."
|
|
12
|
+
import {
|
|
13
|
+
createManifest,
|
|
14
|
+
determineRebuildPages,
|
|
15
|
+
hashEntities,
|
|
16
|
+
loadManifest,
|
|
17
|
+
saveManifest,
|
|
18
|
+
} from "./manifest.js"
|
|
19
|
+
import { generatePages } from "./pages.js"
|
|
20
|
+
import { copyPublicDir } from "./public.js"
|
|
21
|
+
import { generateRSS } from "./rss.js"
|
|
22
|
+
import { generateSitemap } from "./sitemap.js"
|
|
23
|
+
import { createViteConfig } from "./vite-config.js"
|
|
24
|
+
|
|
25
|
+
vi.mock("node:fs/promises")
|
|
26
|
+
vi.mock("vite")
|
|
27
|
+
vi.mock("../router/index.js")
|
|
28
|
+
vi.mock("../scripts/app.js")
|
|
29
|
+
vi.mock("../store/index.js")
|
|
30
|
+
vi.mock("../utils/config.js")
|
|
31
|
+
vi.mock("./manifest.js")
|
|
32
|
+
vi.mock("./pages.js")
|
|
33
|
+
vi.mock("./public.js")
|
|
34
|
+
vi.mock("./rss.js")
|
|
35
|
+
vi.mock("./sitemap.js")
|
|
36
|
+
vi.mock("./vite-config.js")
|
|
37
|
+
|
|
38
|
+
describe("build", () => {
|
|
39
|
+
// Mock console to keep output clean
|
|
40
|
+
vi.spyOn(console, "log").mockImplementation(() => {})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should run a full build sequence", async () => {
|
|
47
|
+
// Setup mocks
|
|
48
|
+
loadConfig.mockResolvedValue({})
|
|
49
|
+
loadManifest.mockResolvedValue(null) // First build
|
|
50
|
+
getPages.mockResolvedValue([{ path: "/" }])
|
|
51
|
+
hashEntities.mockResolvedValue("hash")
|
|
52
|
+
generateStore.mockResolvedValue({})
|
|
53
|
+
generatePages
|
|
54
|
+
.mockResolvedValueOnce([{ path: "/", html: "<html></html>" }])
|
|
55
|
+
.mockResolvedValueOnce([])
|
|
56
|
+
generateApp.mockReturnValue("console.log('app')")
|
|
57
|
+
createViteConfig.mockReturnValue({})
|
|
58
|
+
createManifest.mockResolvedValue({})
|
|
59
|
+
|
|
60
|
+
const result = await build({ rootDir: "src", outDir: "dist" })
|
|
61
|
+
|
|
62
|
+
// Verify sequence
|
|
63
|
+
expect(fs.rm).toHaveBeenCalledWith("dist", { recursive: true, force: true })
|
|
64
|
+
expect(fs.mkdir).toHaveBeenCalledWith("dist", { recursive: true })
|
|
65
|
+
expect(copyPublicDir).toHaveBeenCalled()
|
|
66
|
+
expect(getPages).toHaveBeenCalled()
|
|
67
|
+
expect(generateStore).toHaveBeenCalled()
|
|
68
|
+
expect(generatePages).toHaveBeenCalledTimes(2) // Changed + Skipped (empty)
|
|
69
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
70
|
+
path.normalize("dist/index.html"),
|
|
71
|
+
"<html></html>",
|
|
72
|
+
"utf-8",
|
|
73
|
+
)
|
|
74
|
+
expect(generateApp).toHaveBeenCalled()
|
|
75
|
+
expect(viteBuild).toHaveBeenCalled()
|
|
76
|
+
expect(saveManifest).toHaveBeenCalled()
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({ changed: 1, skipped: 0 })
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should handle incremental builds", async () => {
|
|
82
|
+
const manifest = { entities: "hash" }
|
|
83
|
+
loadManifest.mockResolvedValue(manifest)
|
|
84
|
+
hashEntities.mockResolvedValue("hash")
|
|
85
|
+
|
|
86
|
+
const allPages = [{ path: "/changed" }, { path: "/skipped" }]
|
|
87
|
+
getPages.mockResolvedValue(allPages)
|
|
88
|
+
|
|
89
|
+
determineRebuildPages.mockResolvedValue({
|
|
90
|
+
pagesToBuild: [{ path: "/changed" }],
|
|
91
|
+
pagesToSkip: [{ path: "/skipped" }],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
generatePages
|
|
95
|
+
.mockResolvedValueOnce([{ path: "/changed", html: "<html></html>" }]) // Changed
|
|
96
|
+
.mockResolvedValueOnce([{ path: "/skipped" }]) // Skipped
|
|
97
|
+
|
|
98
|
+
const result = await build({ incremental: true })
|
|
99
|
+
|
|
100
|
+
expect(determineRebuildPages).toHaveBeenCalled()
|
|
101
|
+
expect(generatePages).toHaveBeenCalledTimes(2)
|
|
102
|
+
// Should only write changed pages
|
|
103
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining("changed"),
|
|
105
|
+
expect.any(String),
|
|
106
|
+
expect.any(String),
|
|
107
|
+
)
|
|
108
|
+
expect(result).toEqual({ changed: 1, skipped: 1 })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("should generate sitemap and rss if configured", async () => {
|
|
112
|
+
loadConfig.mockResolvedValue({})
|
|
113
|
+
getPages.mockResolvedValue([])
|
|
114
|
+
generatePages.mockResolvedValue([])
|
|
115
|
+
|
|
116
|
+
await build({
|
|
117
|
+
sitemap: { hostname: "https://example.com" },
|
|
118
|
+
rss: { link: "https://example.com" },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(generateSitemap).toHaveBeenCalled()
|
|
122
|
+
expect(generateRSS).toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
})
|