kiss-ssg 2.0.0-beta.1 → 2.0.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/AIKB/controller-resolver.md +1 -0
- package/AIKB/dependency-graph.md +31 -0
- package/AIKB/kiss-page.md +4 -2
- package/AIKB/kiss.md +125 -122
- package/AIKB/partials.md +34 -32
- package/AIKB/sitemap.md +1 -1
- package/AIKB/testing.md +1 -0
- package/README.md +654 -654
- package/examples/10-asset-pipeline/assets/css/generated.css +1 -1
- package/examples/9-migrated-from-v1/pages/handlebars-instance.hbs +7 -3
- package/examples/9-migrated-from-v1.js +14 -7
- package/lib/controller-resolver.js +60 -15
- package/lib/dependency-graph.js +76 -0
- package/lib/kiss-page.js +15 -4
- package/lib/kiss.js +65 -7
- package/lib/partials.js +56 -28
- package/lib/sitemap.js +5 -1
- package/llms.txt +188 -188
- package/package.json +2 -2
- package/types/dependency-graph.d.ts +28 -0
- package/types/kiss-page.d.ts +4 -2
- package/types/kiss.d.ts +6 -0
- package/types/partials.d.ts +15 -3
|
@@ -22,6 +22,7 @@ Resolves `options.controller` (a function, or a filename to load) and runs it ag
|
|
|
22
22
|
|
|
23
23
|
- Controller files resolve against `process.cwd()` (via `path.resolve`), consistent with every other folder lookup in the codebase (models, partials, layouts, assets). v1 used `require.main.require` (resolved relative to the entry script) but gated the attempt on a cwd-relative `existsSync` check first — so in v1 a differing cwd already caused the existence check to fail before the entry-relative `require` ever ran, meaning v1's entry-relative resolution never actually worked either; this module's cwd-relative resolution is not a behavior change in practice.
|
|
24
24
|
- `fresh: true` (passed by `lib/kiss.js` only while replaying a watch rebuild) busts _two_ caches, because Node keeps two: the ESM loader caches by resolved URL, so the file URL gets a `?v=<mtimeMs>` query appended; the CommonJS loader caches by _filename_ and never sees that query, so a `module.exports = fn` controller would keep returning the stale export unless `require.cache[require.resolve(path)]` is deleted as well. Both are needed — dropping either one leaves one of the two module systems stale. `require` here comes from `createRequire(import.meta.url)` (this module is ESM, so there is no ambient `require`).
|
|
25
|
+
- A module-level `inFlightFreshLoads` map (`Map<resolvedPath, Promise>`) makes sure only **one** fresh load per controller path is ever in flight; a concurrent `fresh` call for the same path returns the pending promise instead of busting the caches a second time. Two `.page()` registrations naming the same **CommonJS** controller are replayed concurrently on every watch rebuild, and the second call's `delete require.cache[filename]` landed while the first `import()` was still translating the module — Node's translator asserts the entry it just populated is still present, so both pages died on `Error [ERR_INTERNAL_ASSERTION] … at loadCJSModuleWithModuleLoad`. Observed on a real site (two pages sharing `faculty-index.js`), never on a cold build (no `fresh`), and never for an ESM controller (the ESM loader keys on the busted URL, so a second `import()` joins the first job) — but ESM goes through the same map anyway, one code path with no type sniffing. Only _in-flight_ loads are shared: the entry is dropped in a `.finally`, so a `fresh` call made after the previous one settled still re-reads the file, which is the whole point of `fresh`. The non-`fresh` path never touches the map. `test/unit/controller-resolver.test.js` pins both halves, and — because vitest rewrites `import()` inside `lib/` to its own module runner, which never reaches the CJS translator — the reproduction case runs the two concurrent loads in a plain `node` child process, where the assertion was deterministic before the fix. The map is **module-level state shared by every `Kiss` instance in the process**, deliberately: it dedupes identical I/O keyed on an absolute path, so two instances loading the same controller file want the same answer, and Node's own module caches it fronts are process-global anyway. Two residuals are knowingly left open: a **non-`fresh`** `import()` racing a `fresh` one of the same CJS file is still unguarded (only `fresh` calls join the map, so the cache-delete can still land mid-translation of a plain load), and the map key is `path.resolve`, not a realpath — on NTFS a case-variant spelling of the same file (`Faculty-Index.js` vs `faculty-index.js`) takes a second slot and the two loads do not share.
|
|
25
26
|
- Cache-busting keys on `mtimeMs`, so an _unchanged_ controller still hits the ESM cache on replay (no re-evaluation cost per rebuild); only edited files reload. Two rewrites within the same filesystem-timestamp tick would collapse to one URL — not a concern in watch mode, where chokidar's `awaitWriteFinish` already delays a rebuild ~100ms past the last write.
|
|
26
27
|
- Without `fresh` (every non-watch build) behaviour is byte-for-byte unchanged: `import()` caches per resolved URL exactly as `require()` cached per path, so a controller changed mid-process is not re-read. `test/unit/controller-resolver.test.js` pins that cached-by-default behaviour alongside the `fresh` cases.
|
|
27
28
|
- Accepts both `export default fn` and CommonJS-style `module.exports = fn` (`import()` surfaces the latter as `mod.default`), via `mod.default ?? mod`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# dependency-graph.js
|
|
2
|
+
|
|
3
|
+
## Responsibility
|
|
4
|
+
|
|
5
|
+
A pure, per-instance index of which pages rendered which partials, populated as a side effect of rendering and read by `Kiss._handleChange` to decide which pages an edited partial or layout can affect.
|
|
6
|
+
|
|
7
|
+
## Public interface
|
|
8
|
+
|
|
9
|
+
- `new DependencyGraph()`
|
|
10
|
+
- `record(page, partial)` — adds the edge both ways; idempotent. `page` is the page's `buildTo`, `partial` its registered name.
|
|
11
|
+
- `clearPage(page)` — removes every edge of that page. A partial it used stays known, possibly with no dependents.
|
|
12
|
+
- `clear()` — forgets everything (a whole-site replay discards the stack, so the graph goes with it).
|
|
13
|
+
- `dependentsOf(partial)` → `string[] | null` — sorted page keys, or **`null` if the partial was never recorded**. `null` and `[]` mean different things to the caller: `null` re-renders every page (the safe fallback), `[]` re-renders none.
|
|
14
|
+
- `usesOf(page)` → `string[]` — sorted partial names that page recorded.
|
|
15
|
+
- `size` — how many partials are known.
|
|
16
|
+
- `toJSON()` → `{ [partial]: pages[] }`, keys and values sorted, so `dependency-graph.json` is stable across builds.
|
|
17
|
+
|
|
18
|
+
## Depends on
|
|
19
|
+
|
|
20
|
+
Nothing.
|
|
21
|
+
|
|
22
|
+
## Depended on by
|
|
23
|
+
|
|
24
|
+
`lib/kiss.js` (owns one per instance; dispatch and the verbose-dev dump), `lib/partials.js` (records), `lib/kiss-page.js` (clears before render; lists in the debug sibling).
|
|
25
|
+
|
|
26
|
+
## Non-obvious behavior
|
|
27
|
+
|
|
28
|
+
- **It is filled by rendering, never by parsing.** A partial is registered as a function that records the invoking page (`lib/partials.js`), so a `{{> (lookup this "name")}}` chosen at render time, a partial reached through another partial, and a layout reached through `{{#extend}}` are all recorded. An inline partial (`{{#*inline}}`) shadows the registered one and records nothing — correct, the page does not depend on the file at that call site.
|
|
29
|
+
- **The page identity travels in Handlebars' data frame**, not in shared state: `KissPage.generate()` renders with `{ data: { kissPage: buildTo } }` and the recording wrapper reads `options.data.kissPage`. Handlebars copies the frame into every nested partial call and handlebars-layouts passes `{ data }` through `extend`/`embed`, so the attribution is correct however many pages render at once or in whatever order. There is no "current page" marker and no ordering invariant.
|
|
30
|
+
- **`null` is the fallback signal.** A partial edited before any page has rendered it (a fresh dev session mid-first-build, a partial only a page that failed would reach) has no record; the dispatcher then re-renders every entry and logs a notice naming the file. A partial whose pages all stopped using it is known with `[]` dependents and re-renders nothing.
|
|
31
|
+
- Keys are `buildTo`, which is stable under `dev: true` (`cleanBuild: 'atomic'` degrades to `true` there, so no staging paths).
|
package/AIKB/kiss-page.md
CHANGED
|
@@ -7,7 +7,7 @@ One page's render logic: resolving its title/slug/path/extension, compiling and
|
|
|
7
7
|
## Public interface
|
|
8
8
|
|
|
9
9
|
- `class KissPage`
|
|
10
|
-
- `new KissPage(view, { hbs, logger } = {})` — `view` is a `.hbs` path (relative to `pagesDir`) or an inline template string.
|
|
10
|
+
- `new KissPage(view, { hbs, logger, graph } = {})` — `view` is a `.hbs` path (relative to `pagesDir`) or an inline template string. `graph` is an optional `DependencyGraph` (`./dependency-graph.js`); without one the render is unchanged.
|
|
11
11
|
- `set path(path)` — sanitizes via `sanitizePath` (`./utils.js`); falsy values are ignored (default `''` kept).
|
|
12
12
|
- `set slug(slug)` / `get slug()` — setter normalizes via `toSlug` (`./utils.js`); falsy values are ignored (default `'index'` kept).
|
|
13
13
|
- `set ext(extension)` — strips a leading `.`, then slugifies via `toSlug` (`./utils.js`), like `path` and `slug`; falsy values are ignored (default `'html'` kept).
|
|
@@ -22,7 +22,7 @@ One page's render logic: resolving its title/slug/path/extension, compiling and
|
|
|
22
22
|
|
|
23
23
|
## Depends on
|
|
24
24
|
|
|
25
|
-
`fs-extra`, `node:path`, `html-minifier-terser` (dynamic `import()`, never at module load); `./utils.js` (`toSlug`, `toTitleCase`, `sanitizePath`), `./logger.js` (fallback logger only), `./config.js` (`DEFAULT_CONFIG.livereloadPort`, the fallback for a `KissPage` built outside `Kiss`).
|
|
25
|
+
`fs-extra`, `node:path`, `html-minifier-terser` (dynamic `import()`, never at module load); `./utils.js` (`toSlug`, `toTitleCase`, `sanitizePath`), `./logger.js` (fallback logger only), `./config.js` (`DEFAULT_CONFIG.livereloadPort`, the fallback for a `KissPage` built outside `Kiss`). The dependency graph arrives as `deps.graph` (`AIKB/dependency-graph.md`) and is referenced only by a type-only `import('./dependency-graph.js')` in the JSDoc — there is no runtime import of that module here.
|
|
26
26
|
|
|
27
27
|
## Depended on by
|
|
28
28
|
|
|
@@ -30,6 +30,8 @@ One page's render logic: resolving its title/slug/path/extension, compiling and
|
|
|
30
30
|
|
|
31
31
|
## Non-obvious behavior
|
|
32
32
|
|
|
33
|
+
- **The render carries the page's identity in Handlebars' data frame**: `template(this.options, { data: { kissPage: this.buildTo } })`. Handlebars copies the frame into every nested partial invocation and handlebars-layouts passes `{ data }` through `extend`/`embed`, so a recording partial (`AIKB/partials.md`) reads `options.data.kissPage` and attributes itself to the right page whatever the render order — there is no "current page" variable and no assumption that pages render one at a time. Before rendering, the page calls `graph.clearPage(buildTo)` so its edge set is exactly what this render reached. `graph` is optional (`deps.graph`); without it the render is byte-identical to before — the extra `data` only adds a key to a frame Handlebars creates anyway.
|
|
34
|
+
- In dev mode the `.json` sibling gains `partials`: the names this page recorded, from `graph.usesOf(buildTo)` — the per-page view of `dependency-graph.json`.
|
|
33
35
|
- `_title` is computed from the _default_ slug (`'index'`) in the constructor, before any page-specific slug is set — so a page without an explicit title or model title falls back to `'Index'`, not a title derived from its own slug. This is a preserved v1 quirk, not a bug to fix.
|
|
34
36
|
- `buildTo` is the value `Kiss._preparePage` uses to detect duplicate pages (same output path) — a duplicate is a build failure there, not a skip.
|
|
35
37
|
- **Every part of `buildTo` is sanitised, `ext` included.** `ext` used to strip only the _first_ `.` (`replace('.', '')`) and never slugify, so unlike `path` (`sanitizePath`) and `slug` (`toSlug`) it carried `/` and `..` straight into the output path: `ext: './../../../escaped/pwned.html'` wrote three levels above the build folder, and the value is reachable from model data through the documented "controller derives the page's options" pattern (review finding C1). It now goes through `toSlug(extension.replace(/^\./, ''))` — `'.xml'` → `'xml'`, `'../x'` → `'x'`.
|