@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.
Files changed (106) hide show
  1. package/.claude/settings.local.json +46 -2
  2. package/.github/workflows/ci.yml +45 -0
  3. package/.prettierrc +1 -1
  4. package/CLAUDE.md +125 -86
  5. package/README.md +3 -3
  6. package/TODO.md +8 -5
  7. package/package.json +12 -5
  8. package/src/build.js +66 -6
  9. package/src/cli.js +1 -0
  10. package/src/config/loadConfig.js +8 -2
  11. package/src/data/computeCreated.js +7 -2
  12. package/src/data/computeImage.js +4 -2
  13. package/src/data/computeLayout.js +3 -3
  14. package/src/data/computeModified.js +7 -1
  15. package/src/data/computePermalink.js +6 -4
  16. package/src/data/computeURL.js +1 -1
  17. package/src/data/initialPageData.js +1 -0
  18. package/src/devServer.js +25 -16
  19. package/src/helpers.js +56 -7
  20. package/src/indexing/lookupHelpers.js +32 -20
  21. package/src/libs/loadNunjucks.js +15 -1
  22. package/src/loaders/baseLoader.js +16 -6
  23. package/src/loaders/jsonLoader.js +1 -1
  24. package/src/loaders/staticLoader.js +1 -1
  25. package/src/transforms/atAttributesContentTransform.js +42 -4
  26. package/src/transforms/nunjucksContentTransform.js +1 -1
  27. package/src/views/computeCollectionDataView.js +8 -2
  28. package/src/views/computeSiteLastUpdatedDataView.js +9 -1
  29. package/src/writers/sitemapContextWriter.js +7 -7
  30. package/test/integration/incremental.test.js +84 -0
  31. package/test/integration/index.test.js +636 -0
  32. package/test/integration/repo/content/about.html +21 -0
  33. package/test/integration/repo/content/blog/draft.md +18 -0
  34. package/test/integration/repo/content/blog/index.md +6 -0
  35. package/test/integration/repo/content/blog/json-post.json +9 -0
  36. package/test/integration/repo/content/blog/post-2024-01.md +21 -0
  37. package/test/integration/repo/content/blog/post-2024-02.md +21 -0
  38. package/test/integration/repo/content/blog/post.md +4 -0
  39. package/test/integration/repo/content/blog/simple.md +9 -0
  40. package/test/integration/repo/content/blog/with-code.md +36 -0
  41. package/test/integration/repo/content/blog/with-images.md +33 -0
  42. package/test/integration/repo/content/contact.html +27 -0
  43. package/test/integration/repo/content/images/graphic.png +0 -0
  44. package/test/integration/repo/content/images/hero.webp +0 -0
  45. package/test/integration/repo/content/images/icon.png +0 -0
  46. package/test/integration/repo/content/images/photo-large.jpg +0 -0
  47. package/test/integration/repo/content/index.js +27 -0
  48. package/test/integration/repo/content/post.md +9 -0
  49. package/test/integration/repo/content/subdirectory/post.md +8 -0
  50. package/test/integration/repo/kiss.config.js +35 -0
  51. package/test/integration/repo/theme/templates/base.njk +30 -0
  52. package/test/integration/repo/theme/templates/collection.njk +33 -0
  53. package/test/integration/repo/theme/templates/page.njk +10 -0
  54. package/test/integration/repo/theme/templates/post.njk +28 -0
  55. package/test/unit/build.private.test.js +886 -0
  56. package/test/unit/build.test.js +2077 -0
  57. package/test/unit/cli.test.js +454 -0
  58. package/test/unit/config/defaultConfig.test.js +185 -0
  59. package/test/unit/config/loadConfig.test.js +322 -0
  60. package/test/unit/data/computeCategory.test.js +177 -0
  61. package/test/unit/data/computeCreated.test.js +199 -0
  62. package/test/unit/data/computeDescription.test.js +168 -0
  63. package/test/unit/data/computeImage.test.js +372 -0
  64. package/test/unit/data/computeLayout.test.js +136 -0
  65. package/test/unit/data/computeMeta.test.js +522 -0
  66. package/test/unit/data/computeModified.test.js +232 -0
  67. package/test/unit/data/computePermalink.test.js +266 -0
  68. package/test/unit/data/computeTitle.test.js +139 -0
  69. package/test/unit/data/computeURL.test.js +77 -0
  70. package/test/unit/data/index.test.js +107 -0
  71. package/test/unit/data/initialPageData.test.js +145 -0
  72. package/test/unit/devServer.test.js +553 -0
  73. package/test/unit/helpers/attributeHelpers.test.js +287 -0
  74. package/test/unit/helpers/buildHelpers.test.js +248 -0
  75. package/test/unit/helpers/computePageId.test.js +90 -0
  76. package/test/unit/helpers/jsonHelpers.test.js +260 -0
  77. package/test/unit/helpers/omitDeep.test.js +233 -0
  78. package/test/unit/helpers/pageHelpers.test.js +445 -0
  79. package/test/unit/helpers/pathHelpers.test.js +136 -0
  80. package/test/unit/helpers/urlHelpers.test.js +92 -0
  81. package/test/unit/indexing/buildPageIndexes.test.js +402 -0
  82. package/test/unit/indexing/index.test.js +72 -0
  83. package/test/unit/indexing/lookupHelpers.test.js +423 -0
  84. package/test/unit/loaders/baseLoader.test.js +332 -0
  85. package/test/unit/loaders/jsLoader.test.js +224 -0
  86. package/test/unit/loaders/jsonLoader.test.js +216 -0
  87. package/test/unit/loaders/markdownLoader.test.js +151 -0
  88. package/test/unit/loaders/staticLoader.test.js +163 -0
  89. package/test/unit/loaders/textLoader.test.js +172 -0
  90. package/test/unit/logger.test.js +355 -0
  91. package/test/unit/transforms/atAttributesContentTransform.test.js +671 -0
  92. package/test/unit/transforms/imageContextTransform.test.js +1311 -0
  93. package/test/unit/transforms/nunjucksContentTransform.test.js +201 -0
  94. package/test/unit/views/computeCategoriesDataView.test.js +365 -0
  95. package/test/unit/views/computeCollectionDataView.test.js +284 -0
  96. package/test/unit/views/computeIterableCollectionDataView.test.js +302 -0
  97. package/test/unit/views/computeSiteLastUpdatedDataView.test.js +234 -0
  98. package/test/unit/views/index.test.js +39 -0
  99. package/test/unit/writers/htmlWriter.test.js +296 -0
  100. package/test/unit/writers/imageWriter.test.js +435 -0
  101. package/test/unit/writers/index.test.js +42 -0
  102. package/test/unit/writers/jsonContextWriter.test.js +436 -0
  103. package/test/unit/writers/rssContextWriter.test.js +567 -0
  104. package/test/unit/writers/sitemapContextWriter.test.js +490 -0
  105. package/test-utils/helpers.js +301 -0
  106. 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
@@ -1,3 +1,3 @@
1
1
  {
2
2
  "semi": false
3
- }
3
+ }
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
- 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.
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
- ## Development Commands
9
+ ## Commands
10
10
 
11
- ### Build the site
12
11
  ```bash
13
- npx kiss build
14
- # Production build:
15
- NODE_ENV=production npx kiss build
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
- ### Development server with auto-reload
19
- ```bash
20
- npx kiss start
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 kiss watch
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
- ### Serve only (no watch)
31
- ```bash
32
- npx kiss serve
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
- ### Linting and formatting
36
- ```bash
37
- npx eslint .
38
- npx prettier --write .
39
- # Note: Prettier config uses no semicolons
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
- ## Architecture
102
+ `config.addPlugin(fn, options)` calls `fn(config, options)` for plugin-style extension.
43
103
 
44
- ### Build Pipeline
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
- The build process (`src/build.js`) follows this lifecycle:
106
+ ### Hook System
47
107
 
48
- 1. **Config Loading**: Loads `kiss.config.js` with defaults from `src/config/defaultConfig.js`
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
- ### Content Organization
110
+ - A plain function: `(options, config, data, buildFlags) => data`
111
+ - An object: `{ action: "run"|"copy"|"exec", handler?, from?, to?, command? }`
56
112
 
57
- - Content lives in `content/` directory
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
- ### Key Data Flow
115
+ ### @attribute System
63
116
 
64
- Pages have a data structure with:
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
- ### Dynamic Computations
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
- Located in `src/data/`, these functions run during data cascade to compute values:
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
- ### Content Loaders
126
+ ### Page Indexing (`src/indexing/`)
79
127
 
80
- Located in `src/loaders/`:
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
- ### Content Transforms
130
+ ### Incremental Build
88
131
 
89
- Located in `src/transforms/`:
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
- ### Writers
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
- Located in `src/writers/`:
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
- ## Configuration
141
+ ## Testing
104
142
 
105
- Main config file is `kiss.config.js`. Key settings:
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
- - `context.site`: Site-wide metadata (url, title, description)
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
- ## Template System
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
- Uses Nunjucks templates in `theme/templates/`:
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
- ## Important Conventions
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
- - Code style: No semicolons (per Prettier config)
123
- - Node.js 20+ required
124
- - Uses CommonJS modules (require/module.exports)
125
- - Async/await for asynchronous operations
126
- - Lodash for utility functions
127
- - Fast-glob for file system operations
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
- TODO
2
- - review code
3
- - try to get read of findBy duplication in atAttributesContentTransform.js
4
- - test deployment on cloudflare pages
5
- - test batch writes
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.10.1",
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": "^9.30.1",
33
- "eslint": "^9.30.1",
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
- console.timeEnd("Build time")
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
- console.timeEnd("Build time")
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
@@ -32,4 +32,5 @@ yargs(hideBin(process.argv))
32
32
  default: false,
33
33
  describe: "Performs incremental builds on watch (experimental)",
34
34
  })
35
+ .strict()
35
36
  .demandCommand(1, "Enter a command").argv
@@ -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 created = pages[pageId][config.defaults.pagePublishedAttribute]
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
  }
@@ -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
- // something went wrong
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.post || "post.njk"
3
+ return config.templates?.post || "post.njk"
4
4
  }
5
5
  if (_meta.isCollection) {
6
- return config.templates.collection || "collection.njk"
6
+ return config.templates?.collection || "collection.njk"
7
7
  }
8
- return config.templates.default || "default.njk"
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 modified = pages[pageId][config.defaults.pageUpdatedAttribute]
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
  }