@slybridges/kiss 0.10.1 → 0.11.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/.claude/settings.local.json +46 -2
- package/.github/workflows/ci.yml +45 -0
- package/.prettierrc +1 -1
- package/CLAUDE.md +125 -86
- package/README.md +3 -3
- package/TODO.md +8 -5
- package/package.json +12 -5
- package/src/build.js +66 -6
- package/src/cli.js +1 -0
- package/src/config/loadConfig.js +8 -2
- package/src/data/computeCreated.js +7 -2
- package/src/data/computeImage.js +4 -2
- package/src/data/computeLayout.js +3 -3
- package/src/data/computeModified.js +7 -1
- package/src/data/computePermalink.js +6 -4
- package/src/data/computeURL.js +1 -1
- package/src/data/initialPageData.js +1 -0
- package/src/devServer.js +25 -16
- package/src/helpers.js +56 -7
- package/src/indexing/lookupHelpers.js +32 -20
- package/src/libs/loadNunjucks.js +15 -1
- package/src/loaders/baseLoader.js +16 -6
- package/src/loaders/jsonLoader.js +1 -1
- package/src/loaders/staticLoader.js +1 -1
- package/src/transforms/atAttributesContentTransform.js +42 -4
- package/src/transforms/nunjucksContentTransform.js +1 -1
- package/src/views/computeCollectionDataView.js +8 -2
- package/src/views/computeSiteLastUpdatedDataView.js +9 -1
- package/src/writers/sitemapContextWriter.js +7 -7
- package/test/integration/incremental.test.js +84 -0
- package/test/integration/index.test.js +636 -0
- package/test/integration/repo/content/about.html +21 -0
- package/test/integration/repo/content/blog/draft.md +18 -0
- package/test/integration/repo/content/blog/index.md +6 -0
- package/test/integration/repo/content/blog/json-post.json +9 -0
- package/test/integration/repo/content/blog/post-2024-01.md +21 -0
- package/test/integration/repo/content/blog/post-2024-02.md +21 -0
- package/test/integration/repo/content/blog/post.md +4 -0
- package/test/integration/repo/content/blog/simple.md +9 -0
- package/test/integration/repo/content/blog/with-code.md +36 -0
- package/test/integration/repo/content/blog/with-images.md +33 -0
- package/test/integration/repo/content/contact.html +27 -0
- package/test/integration/repo/content/images/graphic.png +0 -0
- package/test/integration/repo/content/images/hero.webp +0 -0
- package/test/integration/repo/content/images/icon.png +0 -0
- package/test/integration/repo/content/images/photo-large.jpg +0 -0
- package/test/integration/repo/content/index.js +27 -0
- package/test/integration/repo/content/post.md +9 -0
- package/test/integration/repo/content/subdirectory/post.md +8 -0
- package/test/integration/repo/kiss.config.js +35 -0
- package/test/integration/repo/theme/templates/base.njk +30 -0
- package/test/integration/repo/theme/templates/collection.njk +33 -0
- package/test/integration/repo/theme/templates/page.njk +10 -0
- package/test/integration/repo/theme/templates/post.njk +28 -0
- package/test/unit/build.private.test.js +886 -0
- package/test/unit/build.test.js +2077 -0
- package/test/unit/cli.test.js +454 -0
- package/test/unit/config/defaultConfig.test.js +185 -0
- package/test/unit/config/loadConfig.test.js +322 -0
- package/test/unit/data/computeCategory.test.js +177 -0
- package/test/unit/data/computeCreated.test.js +199 -0
- package/test/unit/data/computeDescription.test.js +168 -0
- package/test/unit/data/computeImage.test.js +372 -0
- package/test/unit/data/computeLayout.test.js +136 -0
- package/test/unit/data/computeMeta.test.js +522 -0
- package/test/unit/data/computeModified.test.js +232 -0
- package/test/unit/data/computePermalink.test.js +266 -0
- package/test/unit/data/computeTitle.test.js +139 -0
- package/test/unit/data/computeURL.test.js +77 -0
- package/test/unit/data/index.test.js +107 -0
- package/test/unit/data/initialPageData.test.js +145 -0
- package/test/unit/devServer.test.js +553 -0
- package/test/unit/helpers/attributeHelpers.test.js +287 -0
- package/test/unit/helpers/buildHelpers.test.js +248 -0
- package/test/unit/helpers/computePageId.test.js +90 -0
- package/test/unit/helpers/jsonHelpers.test.js +260 -0
- package/test/unit/helpers/omitDeep.test.js +233 -0
- package/test/unit/helpers/pageHelpers.test.js +445 -0
- package/test/unit/helpers/pathHelpers.test.js +136 -0
- package/test/unit/helpers/urlHelpers.test.js +92 -0
- package/test/unit/indexing/buildPageIndexes.test.js +402 -0
- package/test/unit/indexing/index.test.js +72 -0
- package/test/unit/indexing/lookupHelpers.test.js +423 -0
- package/test/unit/loaders/baseLoader.test.js +332 -0
- package/test/unit/loaders/jsLoader.test.js +224 -0
- package/test/unit/loaders/jsonLoader.test.js +216 -0
- package/test/unit/loaders/markdownLoader.test.js +151 -0
- package/test/unit/loaders/staticLoader.test.js +163 -0
- package/test/unit/loaders/textLoader.test.js +172 -0
- package/test/unit/logger.test.js +355 -0
- package/test/unit/transforms/atAttributesContentTransform.test.js +671 -0
- package/test/unit/transforms/imageContextTransform.test.js +1311 -0
- package/test/unit/transforms/nunjucksContentTransform.test.js +201 -0
- package/test/unit/views/computeCategoriesDataView.test.js +365 -0
- package/test/unit/views/computeCollectionDataView.test.js +284 -0
- package/test/unit/views/computeIterableCollectionDataView.test.js +302 -0
- package/test/unit/views/computeSiteLastUpdatedDataView.test.js +234 -0
- package/test/unit/views/index.test.js +39 -0
- package/test/unit/writers/htmlWriter.test.js +296 -0
- package/test/unit/writers/imageWriter.test.js +435 -0
- package/test/unit/writers/index.test.js +42 -0
- package/test/unit/writers/jsonContextWriter.test.js +436 -0
- package/test/unit/writers/rssContextWriter.test.js +567 -0
- package/test/unit/writers/sitemapContextWriter.test.js +490 -0
- package/test-utils/helpers.js +301 -0
- package/test-utils/incremental-runner.js +646 -0
|
@@ -2,9 +2,53 @@
|
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
4
|
"Bash(npx eslint:*)",
|
|
5
|
-
"Bash(grep:*)"
|
|
5
|
+
"Bash(grep:*)",
|
|
6
|
+
"Bash(node:*)",
|
|
7
|
+
"Bash(npm test:*)",
|
|
8
|
+
"Read(//private/tmp/**)",
|
|
9
|
+
"Bash(tee:*)",
|
|
10
|
+
"Bash(git checkout:*)",
|
|
11
|
+
"Bash(awk:*)",
|
|
12
|
+
"Bash(npm run test:coverage:*)",
|
|
13
|
+
"WebSearch",
|
|
14
|
+
"Bash(cat:*)",
|
|
15
|
+
"Bash(npx kiss build)",
|
|
16
|
+
"Bash(timeout 30 npm test:*)",
|
|
17
|
+
"Bash(chmod:*)",
|
|
18
|
+
"Bash(bash run-incremental-tests.sh:*)",
|
|
19
|
+
"Bash(wait)",
|
|
20
|
+
"Bash(timeout 10 node:*)",
|
|
21
|
+
"Bash(VERBOSITY=info node test/integration/incremental-runner.js:*)",
|
|
22
|
+
"Bash(npx kiss build:*)",
|
|
23
|
+
"Bash(VERBOSITY=error node test/integration/incremental-runner.js:*)",
|
|
24
|
+
"Bash(VERBOSITY=error node test/utils/incremental-runner.js:*)",
|
|
25
|
+
"Bash(VERBOSITY=error node test-utils/incremental-runner.js:*)",
|
|
26
|
+
"Bash(find:*)",
|
|
27
|
+
"Bash(VERBOSITY=info node:*)",
|
|
28
|
+
"Bash(npx prettier --write test-utils/ test/)",
|
|
29
|
+
"Bash(npm run validate:*)",
|
|
30
|
+
"Bash(npx prettier:*)",
|
|
31
|
+
"Bash(npm run lint:*)",
|
|
32
|
+
"Bash(npm run format:check:*)",
|
|
33
|
+
"Bash(npm run format:*)",
|
|
34
|
+
"Bash(gh pr view:*)",
|
|
35
|
+
"Bash(gh pr:*)",
|
|
36
|
+
"Bash(npm audit:*)",
|
|
37
|
+
"Bash(npm ls:*)",
|
|
38
|
+
"Bash(npm view:*)",
|
|
39
|
+
"WebFetch(domain:github.com)",
|
|
40
|
+
"Bash(npm info:*)",
|
|
41
|
+
"Bash(npm pack:*)",
|
|
42
|
+
"Bash(npm install:*)",
|
|
43
|
+
"Bash(gh issue view:*)",
|
|
44
|
+
"Bash(git -C /Users/sly/projects/kiss log --oneline -30)",
|
|
45
|
+
"Bash(git -C /Users/sly/projects/kiss diff package.json .github/workflows/ci.yml CLAUDE.md)",
|
|
46
|
+
"WebFetch(domain:www.npmjs.com)",
|
|
47
|
+
"Bash(npx @11ty/eleventy-dev-server:*)",
|
|
48
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
49
|
+
"WebFetch(domain:vite.dev)"
|
|
6
50
|
],
|
|
7
51
|
"deny": [],
|
|
8
52
|
"ask": []
|
|
9
53
|
}
|
|
10
|
-
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
# Run on all pull requests, regardless of the target branch
|
|
9
|
+
branches:
|
|
10
|
+
- "*"
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Test on Node.js ${{ matrix.node-version }}
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [20.x, 22.x, 24.x]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout code
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: ${{ matrix.node-version }}
|
|
29
|
+
cache: "npm"
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Run linter
|
|
35
|
+
run: npm run lint
|
|
36
|
+
|
|
37
|
+
- name: Check code formatting
|
|
38
|
+
run: npm run format:check
|
|
39
|
+
|
|
40
|
+
- name: Run tests
|
|
41
|
+
run: npm test
|
|
42
|
+
|
|
43
|
+
- name: Run tests with coverage
|
|
44
|
+
if: matrix.node-version == '20.x'
|
|
45
|
+
run: npm run test:coverage
|
package/.prettierrc
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -4,124 +4,163 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
kiss (Keep It Simple and Static) is a low-tech static site generator built with Node.js. It transforms markdown, HTML, JSON, and JavaScript content into static websites using Nunjucks templates. Always refer to it as "kiss" (lowercase), not "KISS".
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Commands
|
|
10
10
|
|
|
11
|
-
### Build the site
|
|
12
11
|
```bash
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
12
|
+
# Testing
|
|
13
|
+
npm test # Run all tests (unit + integration)
|
|
14
|
+
node --test test/unit/ # Run only unit tests
|
|
15
|
+
node --test test/integration/ # Run only integration tests
|
|
16
|
+
node --test test/unit/build.test.js # Run a single test file
|
|
17
|
+
npm run test:watch # Watch mode
|
|
18
|
+
npm run test:coverage # Coverage report
|
|
19
|
+
|
|
20
|
+
# Code quality
|
|
21
|
+
npm run lint # ESLint
|
|
22
|
+
npm run format # Prettier (auto-fix)
|
|
23
|
+
npm run validate # Both lint + format check
|
|
24
|
+
|
|
25
|
+
# Build & dev
|
|
26
|
+
npx kiss build # Full build
|
|
27
|
+
NODE_ENV=production npx kiss build # Production build
|
|
28
|
+
npx kiss start # Dev server with watch + auto-reload
|
|
29
|
+
npx kiss watch --incremental # Watch with incremental builds
|
|
30
|
+
npx kiss build --verbosity=info # Verbose logging (log|info|success|warn|error)
|
|
16
31
|
```
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Launches build, watches for changes, and auto-reloads browser
|
|
22
|
-
```
|
|
33
|
+
## Code Quality Protocol
|
|
34
|
+
|
|
35
|
+
Before completing ANY task, always run on modified files:
|
|
23
36
|
|
|
24
|
-
### Watch mode only (no server)
|
|
25
37
|
```bash
|
|
26
|
-
npx
|
|
27
|
-
# Use --incremental flag for experimental incremental builds
|
|
38
|
+
npx eslint <files-or-dirs> && npx prettier --write <files-or-dirs> && npm test
|
|
28
39
|
```
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
## Code Conventions
|
|
42
|
+
|
|
43
|
+
- **No semicolons** (Prettier config: `{ "semi": false }`)
|
|
44
|
+
- **CommonJS modules** (`require`/`module.exports`)
|
|
45
|
+
- **Node.js 20+** required
|
|
46
|
+
- Async/await for asynchronous operations
|
|
47
|
+
- Lodash for utility functions, fast-glob for file system operations
|
|
48
|
+
|
|
49
|
+
## Architecture
|
|
50
|
+
|
|
51
|
+
### Build Pipeline (`src/build.js`)
|
|
52
|
+
|
|
53
|
+
The `build(options, lastBuild, version)` function is the entire pipeline. The same function handles both full builds and incremental rebuilds via `buildFlags`. It returns `{ context, config }` which becomes `lastBuild` for the next incremental rebuild.
|
|
54
|
+
|
|
55
|
+
**Stages in order:**
|
|
56
|
+
|
|
57
|
+
1. **Config load** — `loadConfig()` deep-clones `defaultConfig` then calls user's `kiss.config.js` function
|
|
58
|
+
2. **`computeBuildFlags`** — determines which stages to run (all for full build, subset for incremental)
|
|
59
|
+
3. **`loadLibs` hooks** — sets up nunjucks, marked, slugify onto `config.libs.*`
|
|
60
|
+
4. **`preLoad` hooks**
|
|
61
|
+
5. **`loadContent`** — scans files via fast-glob, runs `baseLoader` then specific loader. Files sorted index-first, post-last for proper cascade order
|
|
62
|
+
6. **`postLoad` hooks**
|
|
63
|
+
7. **`computeAllPagesData`** — multi-round computation loop resolving `kissDependencies` (up to `maxComputingRounds`: 10)
|
|
64
|
+
8. **`buildPageIndexes`** — builds 6 Maps for O(1) lookups, stored as `context._pageIndexes`
|
|
65
|
+
9. **`computeDataViews`** — populates `context.collections`, `context.categories`, etc.
|
|
66
|
+
10. **`applyTransforms`** — per-page (`PAGE` scope) and global (`CONTEXT` scope) transforms
|
|
67
|
+
11. **`writeStaticSite`** — dispatches pages to writers by `_meta.outputType`
|
|
68
|
+
12. **`postWrite` hooks**
|
|
69
|
+
|
|
70
|
+
### Data Cascade (Critical to Understand)
|
|
71
|
+
|
|
72
|
+
The data cascade is the most sophisticated part of the codebase.
|
|
73
|
+
|
|
74
|
+
**How it works:** `initialPageData` (`src/data/initialPageData.js`) defines page field defaults as **functions** (the `computeX` functions), not plain values. When `baseLoader` merges parent data into a child page, children inherit unevaluated compute functions from parents. `computeAllPagesData` then resolves these in multiple rounds.
|
|
75
|
+
|
|
76
|
+
**`kissDependencies` mechanism:** Compute functions declare inter-page dependencies as static properties:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
computePermalink.kissDependencies = ["slug", ["_meta.parent", "permalink"]]
|
|
33
80
|
```
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
- A string dep = field on the same page
|
|
83
|
+
- An array dep like `["_meta.parent", "permalink"]` = follow `_meta.parent` as a page ID, check `permalink` on that page
|
|
84
|
+
|
|
85
|
+
If dependencies are still unresolved (still functions), the compute is deferred to the next round. `computePermalink` returns itself when parent's permalink isn't ready yet.
|
|
86
|
+
|
|
87
|
+
**`_no_cascade` convention:** A field named `foo_no_cascade` prevents `foo` from cascading to children. The `_no_cascade` variant replaces the normal key for that page only.
|
|
88
|
+
|
|
89
|
+
**Cascade scoping:** `post.md`/`post.js` data sets data on the collection page but does NOT cascade to sibling pages. Only `index.*` file data cascades to children.
|
|
90
|
+
|
|
91
|
+
### Config System
|
|
92
|
+
|
|
93
|
+
**`kiss.config.js` is a function, not an object.** It receives a mutable `baseConfig` (deep-cloned from defaults) and returns it:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
module.exports = (config) => {
|
|
97
|
+
config.context.site.url = "https://example.com"
|
|
98
|
+
return config
|
|
99
|
+
}
|
|
40
100
|
```
|
|
41
101
|
|
|
42
|
-
|
|
102
|
+
`config.addPlugin(fn, options)` calls `fn(config, options)` for plugin-style extension.
|
|
43
103
|
|
|
44
|
-
|
|
104
|
+
**Namespaced options:** Loaders/transforms/writers declare a `namespace` string. Users override defaults via top-level config keys (e.g., `config.image.widths = [...]`).
|
|
45
105
|
|
|
46
|
-
|
|
106
|
+
### Hook System
|
|
47
107
|
|
|
48
|
-
|
|
49
|
-
2. **Content Loading**: Scans `content/` directory recursively, loading files via loaders
|
|
50
|
-
3. **Data Cascade**: Parent data cascades to children, with dynamic computations at each level
|
|
51
|
-
4. **Content Transformation**: Applies transforms (Nunjucks, @attributes, images)
|
|
52
|
-
5. **Data Views**: Computes derived data (collections, categories, site metadata)
|
|
53
|
-
6. **Writing**: Outputs HTML, optimized images, RSS, sitemap, and JSON context
|
|
108
|
+
Hooks are arrays on `config.hooks` (`loadLibs`, `preLoad`, `postLoad`, `postWrite`). A hook entry can be:
|
|
54
109
|
|
|
55
|
-
|
|
110
|
+
- A plain function: `(options, config, data, buildFlags) => data`
|
|
111
|
+
- An object: `{ action: "run"|"copy"|"exec", handler?, from?, to?, command? }`
|
|
56
112
|
|
|
57
|
-
|
|
58
|
-
- Folder structure maps to URL structure
|
|
59
|
-
- Index files (`index.js/md/html`) provide data cascade to children
|
|
60
|
-
- Single files or directories with `post.md/html` become pages
|
|
113
|
+
Object hooks can have an `incrementalRebuild` function for conditional execution during incremental builds.
|
|
61
114
|
|
|
62
|
-
###
|
|
115
|
+
### @attribute System
|
|
63
116
|
|
|
64
|
-
|
|
65
|
-
- User content fields (title, description, created, etc.)
|
|
66
|
-
- Computed fields via dynamic functions (permalink, URL, image optimization)
|
|
67
|
-
- `_meta` object with internal metadata (id, parent, children, paths)
|
|
68
|
-
- `_html` populated by content transformers
|
|
117
|
+
`@attributes` in page data strings are resolved by `atAttributesContentTransform`:
|
|
69
118
|
|
|
70
|
-
|
|
119
|
+
- `@file:/path` — resolves to permalink of the page loaded from that content file
|
|
120
|
+
- `@permalink:/url/` — validates a permalink exists
|
|
121
|
+
- `@id:some-id` — resolves by page ID + language
|
|
122
|
+
- `@data:site.url` — resolves a lodash path into `context`
|
|
71
123
|
|
|
72
|
-
|
|
73
|
-
- `computePermalink`: Generates URL path from file location
|
|
74
|
-
- `computeTitle`: Auto-generates from filename if not provided
|
|
75
|
-
- `computeImage`: Handles responsive image generation
|
|
76
|
-
- `computeDescription`: Extracts from content if not provided
|
|
124
|
+
**Performance-critical:** The transform uses a JSON string serialization trick (serialize page to JSON, regex match all `@attribute:value` patterns, batch replace). The comments explicitly warn against refactoring to use helper functions due to measured 40% overhead at ~1M+ calls per build. The transform bypasses `lookupHelpers` and uses `context._pageIndexes` Maps directly.
|
|
77
125
|
|
|
78
|
-
###
|
|
126
|
+
### Page Indexing (`src/indexing/`)
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
- `markdownLoader`: Processes .md files with front-matter
|
|
82
|
-
- `jsLoader`: Executes .js files that export data
|
|
83
|
-
- `jsonLoader`: Loads .json data files
|
|
84
|
-
- `textLoader`: Loads .html and other text files
|
|
85
|
-
- `staticLoader`: Copies static assets as-is
|
|
128
|
+
Six Maps built in a single pass after `computeAllPagesData`: `byPermalink`, `byInputPath`, `byIdAndLang`, `byDerivative`, `byParentPermalink`, `byInputSource`. Lookup helpers have O(n) fallbacks for when indexes aren't yet built (during data cascade phase).
|
|
86
129
|
|
|
87
|
-
###
|
|
130
|
+
### Incremental Build
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
- `nunjucksContentTransform`: Processes Nunjucks templates in content
|
|
91
|
-
- `atAttributesContentTransform`: Handles @attribute syntax for dynamic data insertion
|
|
92
|
-
- `imageContextTransform`: Optimizes and creates responsive images
|
|
132
|
+
`computeBuildFlags` branches on file type:
|
|
93
133
|
|
|
94
|
-
|
|
134
|
+
- Content file change → reload content + recompute + transform + write
|
|
135
|
+
- Template file change → skip content reload, re-run transforms + write
|
|
136
|
+
- Config file change → full rebuild
|
|
137
|
+
- `unlink`/`unlinkDir` → full rebuild
|
|
95
138
|
|
|
96
|
-
|
|
97
|
-
- `htmlWriter`: Renders pages using Nunjucks templates
|
|
98
|
-
- `imageWriter`: Generates optimized image variants with Sharp
|
|
99
|
-
- `rssContextWriter`: Creates RSS feed
|
|
100
|
-
- `sitemapContextWriter`: Generates sitemap.xml
|
|
101
|
-
- `jsonContextWriter`: Dumps full site context for debugging
|
|
139
|
+
`buildPageIds` computes the impacted page set: ascendants + page + descendants (except for `post.*` files which don't include descendants).
|
|
102
140
|
|
|
103
|
-
##
|
|
141
|
+
## Testing
|
|
104
142
|
|
|
105
|
-
|
|
143
|
+
- Uses **Node.js built-in `node:test`** framework with `node:assert` (no Jest/Mocha/Vitest)
|
|
144
|
+
- Unit tests in `test/unit/` mirror `src/` structure exactly
|
|
145
|
+
- Integration tests in `test/integration/` use fixture site at `test/integration/repo/`
|
|
146
|
+
- Private `build.js` functions are tested via `build.private.test.js` using `module.exports._functionName` exports
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
- `dirs`: Directory paths (content, public, theme, templates)
|
|
109
|
-
- `hooks`: Extensibility points for custom loaders, transforms, writers
|
|
110
|
-
- `defaults`: Default values and behaviors
|
|
111
|
-
- `dataViews`: Functions to compute derived data
|
|
148
|
+
### Test Utilities (`test-utils/helpers.js`)
|
|
112
149
|
|
|
113
|
-
|
|
150
|
+
Key helpers: `createMockConfig(overrides)`, `createMockPage(overrides)`, `createMockContext(overrides)`, `mockGlobalLogger()`/`restoreGlobalLogger()`, `createCapturingLogger()`, `mockProcessExit()`, `mockProcessArgv(args)`, `clearRequireCache(pathPattern)`, `waitFor(conditionFn, timeoutMs, checkIntervalMs)`, `waitForProcessExit(childProcess, timeoutMs)`, `createTempDir()`/`cleanupTempDir(dir)`, `copyFixtureToTemp(fixturePath)`, `createTestFile(dir, filePath, content)`/`createTestFiles(dir, files)`, `assertFileExists(filePath)`/`assertFileContent(filePath, expected)`, `stripAnsi(string)`.
|
|
114
151
|
|
|
115
|
-
|
|
116
|
-
- Templates have access to full page data and site context
|
|
117
|
-
- Default templates: `default.njk`, `collection.njk`, `post.njk`
|
|
118
|
-
- Custom filters loaded via `src/libs/loadNunjucksFilters.js`
|
|
152
|
+
### Incremental Tests
|
|
119
153
|
|
|
120
|
-
|
|
154
|
+
`test/integration/incremental.test.js` spawns `test-utils/incremental-runner.js` as a child process (produces TAP output). This indirection exists because chokidar/vite pending timers prevent `node:test` from exiting cleanly when run inline.
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
|
|
156
|
+
**When modifying incremental tests:**
|
|
157
|
+
|
|
158
|
+
- ALWAYS include `incremental: true` in the initial build
|
|
159
|
+
- Template changes need 250-500ms delay for chokidar detection
|
|
160
|
+
- Debug with: `VERBOSITY=info node test-utils/incremental-runner.js`
|
|
161
|
+
|
|
162
|
+
## Debugging
|
|
163
|
+
|
|
164
|
+
- `npx kiss build --verbosity=log` for maximum output
|
|
165
|
+
- `jsonContextWriter` outputs `sitedata.json` in public dir with full site data
|
|
166
|
+
- For intermittent test failures: check test isolation, shared state, and file watcher timing
|
package/README.md
CHANGED
|
@@ -102,9 +102,9 @@ NODE_ENV=production npx kiss build
|
|
|
102
102
|
|
|
103
103
|
kiss codebase is small and easy to navigate:
|
|
104
104
|
|
|
105
|
-
- start with `config/defaultConfig.js` to get an understanding of how you can configure your project.
|
|
106
|
-
- then, head over `build.js` `build()` method to understand the lifecycle of a build.
|
|
107
|
-
- finally, head over to `data/initialPageData.js` and scroll down to the bottom to read about the default page metadata and dynamic computations
|
|
105
|
+
- start with `src/config/defaultConfig.js` to get an understanding of how you can configure your project.
|
|
106
|
+
- then, head over to `src/build.js` `build()` method to understand the lifecycle of a build.
|
|
107
|
+
- finally, head over to `src/data/initialPageData.js` and scroll down to the bottom to read about the default page metadata and dynamic computations
|
|
108
108
|
|
|
109
109
|
Alternatively check out [kiss-starter](https://github.com/slybridges/kiss-starter) for a real life minimal example.
|
|
110
110
|
|
package/TODO.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- test
|
|
1
|
+
Road to v1.0
|
|
2
|
+
|
|
3
|
+
✅ Add full test suite (test on all current sites)
|
|
4
|
+
|
|
5
|
+
- Update README with test info (including coverage?)
|
|
6
|
+
- Bump all deps to latest (or remove if possible)
|
|
7
|
+
- 5 moderate npm audit vulns remain (ajv ReDoS in eslint). Track https://github.com/eslint/eslint/issues/20508 for ajv v8 upgrade
|
|
8
|
+
- Add documentation site
|
package/package.json
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slybridges/kiss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Keep It Simple and Static site generator",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kiss": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "node src/index.js"
|
|
10
|
+
"build": "node src/index.js",
|
|
11
|
+
"lint": "eslint .",
|
|
12
|
+
"format": "prettier --write .",
|
|
13
|
+
"format:check": "prettier --check .",
|
|
14
|
+
"validate": "npm run lint && npm run format:check",
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"test:watch": "node --test --watch",
|
|
17
|
+
"test:coverage": "node --experimental-test-coverage --test"
|
|
11
18
|
},
|
|
12
19
|
"author": "Sylvestre Dupont",
|
|
13
20
|
"license": "MIT",
|
|
14
21
|
"dependencies": {
|
|
15
|
-
"browser-sync": "^3.0.4",
|
|
16
22
|
"chalk": "^4.1.2",
|
|
17
23
|
"cheerio": "^1.1.0",
|
|
18
24
|
"chokidar": "^3.6.0",
|
|
@@ -29,8 +35,9 @@
|
|
|
29
35
|
"yargs": "^17.7.2"
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
32
|
-
"@eslint/js": "^
|
|
33
|
-
"
|
|
38
|
+
"@eslint/js": "^10.0.0",
|
|
39
|
+
"vite": "^7.3.1",
|
|
40
|
+
"eslint": "^10.0.0",
|
|
34
41
|
"eslint-config-prettier": "^10.1.5",
|
|
35
42
|
"globals": "^15.15.0",
|
|
36
43
|
"prettier": "^3.6.2"
|
package/src/build.js
CHANGED
|
@@ -19,10 +19,14 @@ const { setGlobalLogger } = require("./logger")
|
|
|
19
19
|
const hasOwnProperty = Object.prototype.hasOwnProperty
|
|
20
20
|
|
|
21
21
|
const build = async (options = {}, lastBuild = {}, version = 0) => {
|
|
22
|
-
console.time("Build time")
|
|
23
|
-
|
|
24
22
|
const { configFile, unsafeBuild, verbosity, watchMode } = options
|
|
25
23
|
|
|
24
|
+
// Only show build time for verbose logging levels
|
|
25
|
+
const showBuildTime = ["log", "info"].includes(verbosity)
|
|
26
|
+
if (showBuildTime) {
|
|
27
|
+
console.time("Build time")
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
setGlobalLogger(verbosity)
|
|
27
31
|
|
|
28
32
|
let { config } = lastBuild
|
|
@@ -152,7 +156,9 @@ const build = async (options = {}, lastBuild = {}, version = 0) => {
|
|
|
152
156
|
)
|
|
153
157
|
} else {
|
|
154
158
|
global.logger.info("Exiting build with errors.")
|
|
155
|
-
|
|
159
|
+
if (showBuildTime) {
|
|
160
|
+
console.timeEnd("Build time")
|
|
161
|
+
}
|
|
156
162
|
process.exit(1)
|
|
157
163
|
}
|
|
158
164
|
}
|
|
@@ -161,7 +167,9 @@ const build = async (options = {}, lastBuild = {}, version = 0) => {
|
|
|
161
167
|
} else {
|
|
162
168
|
global.logger.success(`Build completed!`)
|
|
163
169
|
}
|
|
164
|
-
|
|
170
|
+
if (showBuildTime) {
|
|
171
|
+
console.timeEnd("Build time")
|
|
172
|
+
}
|
|
165
173
|
|
|
166
174
|
return { context, config }
|
|
167
175
|
}
|
|
@@ -684,9 +692,10 @@ const getOptions = (config, namespace, options) => {
|
|
|
684
692
|
|
|
685
693
|
const loadContent = async (config, context, buildFlags) => {
|
|
686
694
|
const { incremental, buildPageIds } = buildFlags
|
|
687
|
-
let pages = context.pages
|
|
688
|
-
let files = []
|
|
689
695
|
const isIncrementalBuild = incremental && buildPageIds?.length > 0
|
|
696
|
+
// For incremental builds, reuse existing pages. For full rebuilds, start fresh.
|
|
697
|
+
let pages = isIncrementalBuild ? context.pages : {}
|
|
698
|
+
let files = []
|
|
690
699
|
|
|
691
700
|
if (isIncrementalBuild) {
|
|
692
701
|
// incremental build: one content file changed
|
|
@@ -795,6 +804,29 @@ const loadContent = async (config, context, buildFlags) => {
|
|
|
795
804
|
page = await handler(file.path, options, page, context, config)
|
|
796
805
|
// relative @attributes to absolute
|
|
797
806
|
page = relativeToAbsoluteAttributes(page, options, config)
|
|
807
|
+
|
|
808
|
+
// Update cascadeData after content is loaded
|
|
809
|
+
// Only index files contribute to cascadeData (data that children inherit)
|
|
810
|
+
// Post files override the page but don't cascade to children
|
|
811
|
+
const inputPathObject = path.parse(file.path)
|
|
812
|
+
const isIndexFile = inputPathObject.base.startsWith("index.")
|
|
813
|
+
const isPostFile = inputPathObject.base.startsWith("post.")
|
|
814
|
+
|
|
815
|
+
if (isIndexFile) {
|
|
816
|
+
// Index files set cascadeData - this is what children will inherit
|
|
817
|
+
// eslint-disable-next-line no-unused-vars
|
|
818
|
+
const { _meta, ...cascadeableData } = page
|
|
819
|
+
page._meta.cascadeData = cascadeableData
|
|
820
|
+
} else if (isPostFile) {
|
|
821
|
+
// Post files don't contribute to cascade - preserve existing cascadeData
|
|
822
|
+
if (pages[page._meta.id]?._meta?.cascadeData) {
|
|
823
|
+
page._meta.cascadeData = pages[page._meta.id]._meta.cascadeData
|
|
824
|
+
}
|
|
825
|
+
} else if (pages[page._meta.id]?._meta?.cascadeData) {
|
|
826
|
+
// Other files preserve cascadeData from previous version
|
|
827
|
+
page._meta.cascadeData = pages[page._meta.id]._meta.cascadeData
|
|
828
|
+
}
|
|
829
|
+
|
|
798
830
|
pages[page._meta.id] = page
|
|
799
831
|
global.logger.log(`- [${handler.name}] loaded '${file.path}'`)
|
|
800
832
|
} catch (err) {
|
|
@@ -1009,3 +1041,31 @@ const writeStaticSite = async (context, config, buildFlags) => {
|
|
|
1009
1041
|
}),
|
|
1010
1042
|
)
|
|
1011
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
/** Private functions - exported for testing only **/
|
|
1046
|
+
|
|
1047
|
+
// The following functions are internal implementation details and should not be
|
|
1048
|
+
// used by external code. They are exported with a leading underscore only to
|
|
1049
|
+
// enable comprehensive unit testing. These exports may change without notice.
|
|
1050
|
+
|
|
1051
|
+
module.exports._applyTransforms = applyTransforms
|
|
1052
|
+
module.exports._computeBuildFlags = computeBuildFlags
|
|
1053
|
+
module.exports._computeBuildPageIDs = computeBuildPageIDs
|
|
1054
|
+
module.exports._computeIncrementalHookBuildFlag =
|
|
1055
|
+
computeIncrementalHookBuildFlag
|
|
1056
|
+
module.exports._computeDataViews = computeDataViews
|
|
1057
|
+
module.exports._computePageData = computePageData
|
|
1058
|
+
module.exports._computeAllPagesData = computeAllPagesData
|
|
1059
|
+
module.exports._countPendingDependencies = countPendingDependencies
|
|
1060
|
+
module.exports._directoryCollectionLoader = directoryCollectionLoader
|
|
1061
|
+
module.exports._isComputableValue = isComputableValue
|
|
1062
|
+
module.exports._findMatchingLoaderId = findMatchingLoaderId
|
|
1063
|
+
module.exports._getFiles = getFiles
|
|
1064
|
+
module.exports._getOptions = getOptions
|
|
1065
|
+
module.exports._loadContent = loadContent
|
|
1066
|
+
module.exports._runConfigHooks = runConfigHooks
|
|
1067
|
+
module.exports._runCopyHook = runCopyHook
|
|
1068
|
+
module.exports._runExecHook = runExecHook
|
|
1069
|
+
module.exports._sortFiles = sortFiles
|
|
1070
|
+
module.exports._runHandlerHook = runHandlerHook
|
|
1071
|
+
module.exports._writeStaticSite = writeStaticSite
|
package/src/cli.js
CHANGED
package/src/config/loadConfig.js
CHANGED
|
@@ -11,12 +11,18 @@ const loadConfig = (options = {}) => {
|
|
|
11
11
|
setGlobalLogger(options.verbosity) // loadConfig is called in devServer outside of dev
|
|
12
12
|
const relativeConfigFile = path.relative(__dirname, configFile)
|
|
13
13
|
let configFunc
|
|
14
|
+
let config
|
|
14
15
|
try {
|
|
15
16
|
configFunc = require(relativeConfigFile)
|
|
17
|
+
if (typeof configFunc !== "function") {
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
`Config file must export a function, got ${typeof configFunc}`,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
config = configFunc(baseConfig)
|
|
16
23
|
} catch (e) {
|
|
17
|
-
throw new Error(`Error loading '${configFile}': ${e}
|
|
24
|
+
throw new Error(`Error loading '${configFile}': ${e}`, { cause: e })
|
|
18
25
|
}
|
|
19
|
-
let config = configFunc(baseConfig)
|
|
20
26
|
if (configFile !== baseConfig.configFile) {
|
|
21
27
|
config.configFile = configFile
|
|
22
28
|
}
|
|
@@ -2,7 +2,12 @@ const computeCreated = ({ _meta }, config, { pages }) => {
|
|
|
2
2
|
// take the earliest created date of all descendants
|
|
3
3
|
if (_meta.isCollection) {
|
|
4
4
|
const earliest = _meta.descendants.reduce((earliest, pageId) => {
|
|
5
|
-
const
|
|
5
|
+
const page = pages[pageId]
|
|
6
|
+
if (!page) {
|
|
7
|
+
// page doesn't exist
|
|
8
|
+
return earliest
|
|
9
|
+
}
|
|
10
|
+
const created = page[config.defaults.pagePublishedAttribute]
|
|
6
11
|
// assume an date is a valid Date object if it has a getMonth function
|
|
7
12
|
if (!created || typeof created?.getMonth !== "function") {
|
|
8
13
|
// invalid or net yet computed
|
|
@@ -14,7 +19,7 @@ const computeCreated = ({ _meta }, config, { pages }) => {
|
|
|
14
19
|
return earliest
|
|
15
20
|
}
|
|
16
21
|
}
|
|
17
|
-
|
|
22
|
+
// fallback to own created date
|
|
18
23
|
if (_meta.fileCreated) {
|
|
19
24
|
return _meta.fileCreated
|
|
20
25
|
}
|
package/src/data/computeImage.js
CHANGED
|
@@ -91,9 +91,11 @@ const getImagePermalink = (
|
|
|
91
91
|
// external image, data URI or @attribute that will be resolved later
|
|
92
92
|
return src
|
|
93
93
|
}
|
|
94
|
-
const pageFound = getPageFromSource(src, page, pages, config
|
|
94
|
+
const pageFound = getPageFromSource(src, page, pages, config, {
|
|
95
|
+
throwIfNotFound: false,
|
|
96
|
+
})
|
|
95
97
|
if (!pageFound) {
|
|
96
|
-
//
|
|
98
|
+
// image page not found, return the src as-is
|
|
97
99
|
return src
|
|
98
100
|
}
|
|
99
101
|
return pageFound.permalink
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const computeLayout = ({ _meta }, config) => {
|
|
2
2
|
if (_meta.isPost) {
|
|
3
|
-
return config.templates
|
|
3
|
+
return config.templates?.post || "post.njk"
|
|
4
4
|
}
|
|
5
5
|
if (_meta.isCollection) {
|
|
6
|
-
return config.templates
|
|
6
|
+
return config.templates?.collection || "collection.njk"
|
|
7
7
|
}
|
|
8
|
-
return config.templates
|
|
8
|
+
return config.templates?.default || "default.njk"
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
computeLayout.kissDependencies = ["_meta.isCollection", "_meta.isPost"]
|
|
@@ -2,7 +2,12 @@ const computeModified = ({ _meta }, config, { pages }) => {
|
|
|
2
2
|
// take the latest modified date of all descendants
|
|
3
3
|
if (_meta.isCollection) {
|
|
4
4
|
const latest = _meta.descendants.reduce((latest, pageId) => {
|
|
5
|
-
const
|
|
5
|
+
const page = pages[pageId]
|
|
6
|
+
if (!page) {
|
|
7
|
+
// page doesn't exist
|
|
8
|
+
return latest
|
|
9
|
+
}
|
|
10
|
+
const modified = page[config.defaults.pageUpdatedAttribute]
|
|
6
11
|
// assume an date is a valid Date object if it has a getMonth function
|
|
7
12
|
if (!modified || typeof modified?.getMonth !== "function") {
|
|
8
13
|
// invalid or net yet computed
|
|
@@ -14,6 +19,7 @@ const computeModified = ({ _meta }, config, { pages }) => {
|
|
|
14
19
|
return latest
|
|
15
20
|
}
|
|
16
21
|
}
|
|
22
|
+
// fallback to own modified date
|
|
17
23
|
if (_meta.fileModified) {
|
|
18
24
|
return _meta.fileModified
|
|
19
25
|
}
|