@zvndev/darkmown 2.10.0 → 2.10.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/AGENTS.md +4 -4
- package/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/SECURITY.md +1 -0
- package/docs/cli.md +1 -1
- package/docs/spec-alignment.md +9 -1
- package/package.json +2 -2
- package/site/pages/benchmarks/index.wd +1 -1
- package/site/pages/playground/index.js +58 -54
- package/src/api-runner.js +80 -8
- package/src/builder.js +4 -2
- package/src/cli.js +13 -5
- package/src/compiler/state.js +75 -0
- package/src/errors.js +50 -3
- package/src/font-subset.js +25 -7
- package/src/image-variant-resize.js +4 -1
- package/src/image-variants.js +9 -4
- package/src/router.js +16 -0
- package/src/skin.js +111 -45
- package/src/templates/dashboard/README.md +3 -1
- package/src/templates/landing/site/pages/index.skin +7 -0
- package/src/templates/landing/site/pages/index.wd +10 -3
- package/src/templates/store/README.md +5 -2
- package/src/templates/store/site/_/card.wd +19 -0
- package/src/templates/store/site/_/products.json +6 -6
- package/src/templates/store/site/pages/index.skin +17 -2
- package/src/templates/store/site/pages/index.wd +3 -12
- package/types/api-runner.d.ts +20 -4
- package/types/errors.d.ts +6 -4
- package/types/font-subset.d.ts +7 -3
- package/types/image-variants.d.ts +7 -3
- package/types/router.d.ts +5 -0
- package/types/skin.d.ts +9 -4
package/AGENTS.md
CHANGED
|
@@ -142,7 +142,7 @@ Count: { count } · Total: { total }
|
|
|
142
142
|
- `:store name = value` is global by name, shared across pages and tabs. **Persisted by default**; add `ephemeral` to opt out.
|
|
143
143
|
- `:computed name = <expression>` derives from other state: names, numbers, `+ - * /`, comparisons, and the five aggregates `sum(list, field)` / `avg` / `min` / `max` / `count(list)`. It takes no `persist`/`ephemeral` (that is a compile error).
|
|
144
144
|
- Values are JSON: string, number, `true`/`false`/`null`, array, object. An array or object literal may span lines; the `persist`/`ephemeral` token then goes after the closing bracket. A blank line inside the literal ends it.
|
|
145
|
-
- A **seed is stored verbatim**. `:state b = a` where `a` is declared state is `[WD251]`, not a reference: derive it with `:computed b = a`, or quote it to keep the literal word. A plain undeclared word is still fine (`:state status = draft`).
|
|
145
|
+
- A **seed is stored verbatim**. `:state b = a` where `a` is declared state is `[WD251]`, not a reference: derive it with `:computed b = a`, or quote it to keep the literal word. A plain undeclared word is still fine (`:state status = draft`). A seed that looks secret-shaped (a private-key header, a known token prefix with a real body, a JWT, a long mixed-case unbroken token) earns a build hint, never an error: seed `{}` or `""` and fill the value from an `api/` endpoint at runtime.
|
|
146
146
|
- `persist` and `ephemeral` on one line, or the same modifier twice, is `[WD261]`. A declaration named `__proto__`, `constructor`, or `prototype` is `[WD250]` on every declaring directive.
|
|
147
147
|
- A declaration inside an `:if` branch that starts **closed** is hydrated when the branch opens, `persist` included. The seed is claimed once per key, so re-opening the branch does not reset the value.
|
|
148
148
|
|
|
@@ -329,7 +329,7 @@ Children compile in the caller's scope and land at the include's standalone { ch
|
|
|
329
329
|
```
|
|
330
330
|
- `@include /partial.wd [with key="value" …]` inlines a file from `site/_/` or `site/pages/`. Paths are **static literals**; there is no dynamic include path. Includes are compile-time macros with one slot (below); a missing argument with no default renders `{ title }` literally.
|
|
331
331
|
- **`defaults:` frontmatter** (include files only) sets fallback values for `with` arguments: `defaults: [title="Untitled", cta="Learn more"]`. The caller's `with` value always wins; values are strings (`count="3"` is `"3"`, while a caller's `with count=5` stays typed); entries split on top-level commas only, so `cta="Learn more, today"` works. A malformed `defaults:` is `[WD613]`.
|
|
332
|
-
- **Block form:** `@include /card.wd with title="Pricing"` … `@endinclude` compiles the enclosed lines once in the CALLER's scope (its `:state`, and the loop item when the call sits inside a `@loop`) and inlines them where the include puts a standalone `{ children }` line. Zero runtime cost. Called without a block, the slot renders empty; a block given to an include with no `{ children }` line warns that the content is dropped.
|
|
332
|
+
- **Block form:** `@include /card.wd with title="Pricing"` … `@endinclude` compiles the enclosed lines once in the CALLER's scope (its `:state`, and the loop item when the call sits inside a `@loop`) and inlines them where the include puts a standalone `{ children }` line. Zero runtime cost. Called without a block, the slot renders empty; a block given to an include with no `{ children }` line warns that the content is dropped. `with` arguments resolve at expansion time, so they cannot read a REACTIVE loop's row (`[WD604]`); read the loop variable directly in the children lines, which compile in caller scope.
|
|
333
333
|
- `{ children }` is a standalone line only: mid-paragraph or fenced it stays literal, and on a top-level page it is literal text. A `.md` include cannot host a slot. An include may place `{ children }` more than once; the children still compile once.
|
|
334
334
|
- **Pairing:** `@endinclude` closes the nearest preceding unpaired `@include`, so an inline `@include` between opener and closer consumes the pairing (keep inline includes outside a block's span). One level of block nesting is supported; deeper is `[WD616]`, and a stray closer is `[WD614]`. There is NO unclosed-block error: an unpaired opener is simply the inline form, which must stay legal.
|
|
335
335
|
- `::: [tag] [.class …] [#id] [.class when …] [role="…"] [aria-…="…"] [title="…"] … :::` groups content. A container named `section`, `nav`, or `main` emits that real element; any other name is a `<div>` with that class. State declared inside a section is scoped to it (`sectionId:name`).
|
|
@@ -712,7 +712,7 @@ Every author-facing compile error opens with a stable `WDxxx` code and names the
|
|
|
712
712
|
|
|
713
713
|
Codes are grouped by subsystem: `WD0xx` source and frontmatter, `WD1xx` loops and collections, `WD2xx` state and expressions, `WD3xx` button/effect/timer actions, `WD4xx` forms, `WD5xx` fetching and URL safety, `WD6xx` includes and page structure, `WD7xx` media, `WD8xx` skins, `WD9xx` project and CLI.
|
|
714
714
|
|
|
715
|
-
The ones you are most likely to hit while writing new pages: `WD191` (a reactive `@loop` over `|` table rows), `WD250` (a reserved declaration name), `WD251` (`:state b = a` where `a` is state), `WD260` (`from-url` on `:store`/`:theme`), `WD261` (`persist` and `ephemeral` together), `WD315` (`:every`/`:effect` inside a reactive loop), `WD450` (a bound field with no state), `WD451` (a bound `:checkbox` with several options), `WD452` (a file field in a GET form), `WD650`/`WD651` (a non-whitelisted or unquoted attribute), `WD950` (a bad `rss_limit`). Every `Use:` hint that contains `[bracket]` placeholders ends with a concrete `— e.g. <valid line>`: **copy the example, never the placeholder.** `darkmown catalog --llms-full` prints every code with its cause and fix.
|
|
715
|
+
The ones you are most likely to hit while writing new pages: `WD191` (a reactive `@loop` over `|` table rows), `WD250` (a reserved declaration name), `WD251` (`:state b = a` where `a` is state), `WD260` (`from-url` on `:store`/`:theme`), `WD261` (`persist` and `ephemeral` together), `WD315` (`:every`/`:effect` inside a reactive loop), `WD450` (a bound field with no state), `WD451` (a bound `:checkbox` with several options), `WD452` (a file field in a GET form), `WD650`/`WD651` (a non-whitelisted or unquoted attribute), `WD804`-`WD807` (a malformed `@font-face` or `@keyframes` block: a stray name on `@font-face`, a missing or invalid `@keyframes` name, a non-keyframe child, or a keyframe with no declarations), `WD950` (a bad `rss_limit`). Every `Use:` hint that contains `[bracket]` placeholders ends with a concrete `— e.g. <valid line>`: **copy the example, never the placeholder.** `darkmown catalog --llms-full` prints every code with its cause and fix.
|
|
716
716
|
|
|
717
717
|
## Hard rules — do not break these
|
|
718
718
|
|
|
@@ -723,7 +723,7 @@ The ones you are most likely to hit while writing new pages: `WD191` (a reactive
|
|
|
723
723
|
5. **Rename = delete.** Upgrading `.md`→`.wd` means removing the `.md`. Never leave both.
|
|
724
724
|
6. **Declare before you bind.** `:state` (or `:form into x`) must appear before any `{ x }` / `:if x` that reads it.
|
|
725
725
|
7. **Whitelisted grammars only.** Button/`:computed`/predicate vocabularies are fixed forms; arbitrary logic goes in a `.js` escape hatch.
|
|
726
|
-
8. **Never write a secret into a page.** `:state`/`:store` seeds are inlined into public HTML. Tokens and keys arrive at runtime from an `api/` endpoint.
|
|
726
|
+
8. **Never write a secret into a page.** `:state`/`:store` seeds are inlined into public HTML. Tokens and keys arrive at runtime from an `api/` endpoint. The compiler backs this rule with a build hint when a seed looks secret-shaped (a hint, never an error).
|
|
727
727
|
|
|
728
728
|
## Minimal complete page
|
|
729
729
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
|
|
4
4
|
|
|
5
|
+
## 2.10.1 (2026-10-14)
|
|
6
|
+
|
|
7
|
+
A patch release of hardening and contract repair: a security pass over everything the build publishes and serves, the skin at-rule errors gaining their registry codes, a compiler hint for secret-shaped state seeds, and a polish sweep over the playground, templates, and VS Code extension. Behavior tightening an author could notice is called out per item.
|
|
8
|
+
|
|
9
|
+
- **SECURITY: font subsetting refuses symlinked fonts.** `resolveFontUrl` now skips symlinks entirely (matching asset publishing and the image pre-pass, which never publish symlinks) and additionally requires canonical realpath containment inside `site/pages`/`site/_`, reusing the `@include` sandbox. A font linked in from outside the project is silently skipped: no subset, no CSS rewrite, no bytes in dist.
|
|
10
|
+
- **SECURITY / AUTHOR-VISIBLE: route names reject whitespace and control characters.** A page filename that would put `\s` or a control character into the route now fails the build with an error naming the file, because route strings are written verbatim into `dist/_headers` and `llms.txt`, where such a character would corrupt the file. A file named `my post.wd` previously routed; it now fails, so rename it. (The throw is deliberately uncoded for now; the router group has `WD908` reserved for it.)
|
|
11
|
+
- **SECURITY: `api/` request bodies are capped at 10 MB** under `darkmown dev` and `darkmown serve`; over cap answers `413 {"ok": false, "error": "Request body too large"}` (at exactly 10 MB the body is still buffered as before). A malformed percent-encoded dynamic segment (`/api/users/%E0%A4%A` against `api/users/[id].js`) now answers `400 {"ok": false, "error": "Malformed URL encoding"}` instead of a 500 with a stack trace.
|
|
12
|
+
- **SECURITY: `darkmown serve` no longer discloses handler error details.** `dev` keeps real error messages in the 500 JSON (and stack traces in its server catch), because the author is the audience on their own machine. `serve` now mirrors production and the generated Cloudflare worker: generic `{"ok": false, "error": "Internal error"}` 500s, with unexpected failures logged server-side only.
|
|
13
|
+
- **SECURITY: the build workers are bounded.** Both spawned workers (image variants, font subsetting) run with a 120 s timeout, and a killed worker reads as the toolchain being unavailable (one hint, no variants or subsets, never a build failure). The image resize worker passes `limitInputPixels: 1e8` (100 MP) to sharp, so a decompression-bomb-shaped image fails as a per-file hint.
|
|
14
|
+
- **Error-contract repair: the skin at-rule errors are coded `WD804`-`WD807`.** The nine `@font-face`/`@keyframes` throw sites (a named `@font-face`, a nested or empty block, a missing or invalid `@keyframes` name, a non-keyframe child, nested declarations, an empty keyframe or selector set) now carry `{ code, file, line, hint, example }` on `err.wd`, with the builder threading the real path and true line numbers (the comment strip in `compileSkin` now preserves newlines; compiled CSS output is unchanged). The new examples are multi-line block constructs, and the generated `docs/errors.md` tables render a multi-line `Use:` example as one code span per line joined by `<br>`, so the rows stay intact. The drift guard also tightened: its scan now matches `return new Error(` as well as `throw new Error(`, so a factory can no longer evade it. `WD801`-`WD803` stay location-less, as before.
|
|
15
|
+
- **Compiler hint: a secret-looking `:state`/`:store` seed is called out at build time.** Seeds are inlined into the page's public HTML by design, so a pasted key publishes to every reader. The hint (never an error, at most once per declaration, string values nested in array/object seeds included) reads: `hint: <file>:<line>: :state "<name>" seed looks like a secret — it will be inlined into public HTML. Seed empty and fill it from an api/ endpoint at runtime.` The heuristic is deliberately tight (private-key headers, known token prefixes with a real token body, JWTs, and long mixed-case unbroken tokens fire; the documented `:store session = {}` pattern, URLs, file paths, kebab slugs, hex hashes, and UUIDs stay quiet). **An author could notice this**: a real key pasted as a seed now prints a hint on every build.
|
|
16
|
+
- **Polish: the playground bundle is freshness-gated.** `scripts/build-playground.mjs --check` byte-compares a fresh in-memory build against the committed bundle (mirroring the runtime-artifact gate), and the check now runs in CI and in the prepublish chain, so the playground can never drift from the compiler it bundles. The bundle was regenerated and compiles the 2.10 features (include blocks, `defaults:`, `{ children }`, `$build.*`).
|
|
17
|
+
- **Polish: templates updated.** `landing` replaces its emoji feature icons with inline SVGs (and carries `html: true` for them); `store` replaces emoji tiles with styled monograms, and its product card is now a real component, `site/_/card.wd` with `defaults: [note="Ships in 2 days"]` and a `{ children }` slot holding the add-to-cart button. Worth knowing when you copy the pattern: `with` arguments resolve at expansion time and cannot see a REACTIVE loop's row (`WD604`), so the card reads the loop variable directly in its children. Both templates' READMEs no longer claim `darkmown serve` is static-only.
|
|
18
|
+
- **Polish: the VS Code extension scopes `@endinclude`** and ships an "Include block (children slot)" snippet (prefix `@include block`). The extension version is unchanged and the new snippet ships with the next publish.
|
|
19
|
+
- **Polish: stale text fixes.** The benchmarks page's fonts bullet now states that 2.10 ships build-time subsetting (with the measured 1.07 MB TTF to 288 KB woff2 figure) instead of calling it a roadmap item, and the spec-alignment audit's current claim reads v2.10.0.
|
|
20
|
+
|
|
5
21
|
## 2.10.0 (2026-10-14)
|
|
6
22
|
|
|
7
23
|
A release about composition and the last of the rebuild's asset costs. Includes grew the two things a component model needs (default arguments and a children slot) without leaving the compile-time macro model: no runtime marker, no `src/runtime.js` change, a static page stays static. Fonts now subset to woff2 at build time, closing the 7x gap the client rebuild measured. And a security pass hardened four publisher gaps, plus the shipped CSP now follows each page's `:embed` hosts. Three new error codes (`WD613`, `WD614`, `WD616`; `WD615` is deliberately unassigned), no breaking change.
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ defaults: [title="Untitled"]
|
|
|
162
162
|
|
|
163
163
|
Rules worth knowing:
|
|
164
164
|
|
|
165
|
-
- **Caller scope, build time.** Children lines compile as if written where the call is, so `Price: { product.price | money }` inside a loop's block sees the loop row, and the whole thing stays zero-JS when the source is static.
|
|
165
|
+
- **Caller scope, build time.** Children lines compile as if written where the call is, so `Price: { product.price | money }` inside a loop's block sees the loop row, and the whole thing stays zero-JS when the source is static. One edge to know: `with` arguments resolve at expansion time, so they cannot read a REACTIVE loop's row (`[WD604]`); a component inside a reactive loop reads the loop variable directly in its children lines instead.
|
|
166
166
|
- **The slot is a standalone line.** `{ children }` written mid-paragraph is literal text, one inside a fenced code block is documentation rather than a slot, and one on a top-level page (not an include) stays literal too. A `.md` include cannot host a slot at all.
|
|
167
167
|
- **Called without a block, the slot renders empty.** An include may place `{ children }` more than once; the children still compile once, so state a child line declares is declared once in the caller.
|
|
168
168
|
- **A block given to an include that never uses `{ children }` is dropped with a build warning**, so a forgotten slot is loud rather than silent.
|
|
@@ -965,7 +965,7 @@ The first line renders *Pick a file first.*, not *Error: HTTP 422*. Both keys ar
|
|
|
965
965
|
|
|
966
966
|
`headers=<key>` sends a state object as request headers; pair it with `:store` to keep the token across reloads.
|
|
967
967
|
|
|
968
|
-
**Seed the store empty.** A `:state`/`:store` value is a *build-time* seed: it is inlined into the page's public HTML, so a literal token written into a `.wd` file ships to every visitor. The token has to arrive at runtime, from your own `api/` endpoint:
|
|
968
|
+
**Seed the store empty.** A `:state`/`:store` value is a *build-time* seed: it is inlined into the page's public HTML, so a literal token written into a `.wd` file ships to every visitor. The token has to arrive at runtime, from your own `api/` endpoint. Since 2.10.1 the compiler backs this up with a build hint (never an error) when a seed looks secret-shaped: a private-key header, a known token prefix with a real token body, a JWT, or a long mixed-case unbroken token:
|
|
969
969
|
|
|
970
970
|
```wd
|
|
971
971
|
:store session = {}
|
package/SECURITY.md
CHANGED
|
@@ -30,6 +30,7 @@ Things Darkmown deliberately guards at compile time and runtime:
|
|
|
30
30
|
- **Private page assets stay private.** Page-colocated assets under `site/pages` are copied only when their relative path has no hidden segment (`.`, `-`, or `_`) and is not a symlink. This keeps `.env`, `_private/*`, `-draft/*`, and symlink-to-outside files out of `dist`.
|
|
31
31
|
- **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout.
|
|
32
32
|
- **Static server path containment.** The dev/preview servers resolve requests strictly inside `dist`.
|
|
33
|
+
- **Build caches are trusted input.** The `node_modules/.cache/darkmown-*` directories (image variants, font subsets) are copied into `dist` verbatim when their content-hash keys match, so a poisoned shared CI cache ships attacker bytes. Pin and verify cache artifacts in CI, or delete the directories to rebuild from source.
|
|
33
34
|
|
|
34
35
|
## The #1 footgun: the raw HTML opt-in (`html: true`)
|
|
35
36
|
|
package/docs/cli.md
CHANGED
|
@@ -112,7 +112,7 @@ Serves the already-built `dist` directory for local preview. Run `darkmown build
|
|
|
112
112
|
|
|
113
113
|
Since 2.8 the preview is at parity with dev for two things:
|
|
114
114
|
|
|
115
|
-
- **`api/` functions run.** A request to `/api/*` that matches a handler under the project's top-level `api/` directory executes it (same contract and file routing as `darkmown dev` and the production hosts: `api/echo.js` → `/api/echo`, `api/users/[id].js` → `/api/users/:id`); anything else falls through to static serving. `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos that worked in dev now work in the preview too, and editing a handler is picked up without a restart. A project with no `api/` directory behaves exactly as before.
|
|
115
|
+
- **`api/` functions run.** A request to `/api/*` that matches a handler under the project's top-level `api/` directory executes it (same contract and file routing as `darkmown dev` and the production hosts: `api/echo.js` → `/api/echo`, `api/users/[id].js` → `/api/users/:id`); anything else falls through to static serving. `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos that worked in dev now work in the preview too, and editing a handler is picked up without a restart. A project with no `api/` directory behaves exactly as before. Since 2.10.1 the local runner matches production hygiene: request bodies are capped at 10 MB (`413` over cap), a malformed percent-encoded dynamic segment answers `400`, and under `serve` a throwing handler answers the generic `{"ok": false, "error": "Internal error"}` while the real error is logged server-side only. `darkmown dev` keeps the verbose message, because in dev the developer is the audience.
|
|
116
116
|
- **Text responses are compressed.** The server negotiates off the request's `Accept-Encoding` (brotli preferred, then gzip, q-values honored) and sets `Vary: Accept-Encoding`, appending to an existing `Vary` rather than clobbering it. Compressible: `text/*`, `application/javascript`, `application/json`, `application/xml`, and `image/svg+xml`, across static files, the built 404 page, and `api/` responses (a handler that set its own `Content-Encoding` is never double-compressed). Images, video, audio, and fonts are never compressed. `darkmown dev` is intentionally uncompressed, byte-identical to 2.7.
|
|
117
117
|
|
|
118
118
|
### `darkmown catalog [--llms|--llms-full]`
|
package/docs/spec-alignment.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
This audit compares the current Darkmown implementation with the original MARROW/WhateverDown vision and the follow-up decisions made on 2026-06-11.
|
|
4
4
|
|
|
5
|
+
## 2.10.1 additions: hardening and contract repair (2026-10-14)
|
|
6
|
+
|
|
7
|
+
A patch release: nothing in it moves the authoring surface, it tightens what the build publishes and repairs the error contract the 2.8.0 skin at-rules shipped without.
|
|
8
|
+
|
|
9
|
+
- **The security pass tightened everything the build publishes or serves.** Font subsetting refuses symlinked fonts through the shared `@include` sandbox, route names reject whitespace and control characters before they can corrupt `dist/_headers`, local `api/` bodies cap at 10 MB (413) with a 400 for malformed encoding, `darkmown serve` mirrors production's generic 500s (dev keeps verbose errors for its real audience), and both build workers run bounded (120 s timeouts; sharp caps input pixels at 100 MP).
|
|
10
|
+
- **The skin at-rule errors joined the registry as WD804-WD807**, with true file:line threaded from the builder and multi-line examples rendering in the generated docs table. The drift guard now also catches `return new Error(` factories, so the coded-error contract cannot be quietly evaded again.
|
|
11
|
+
- **A compiler hint nudges secret-shaped `:state`/`:store` seeds** toward the documented seed-empty pattern (a hint, never an error, with a deliberately tight heuristic that ignores the documented auth pattern, URLs, hashes, and UUIDs), and a polish sweep gated playground-bundle freshness in CI and rebuilt the templates on the 2.10 component model: the store card is a real `defaults:` + `{ children }` component, which is now the reference implementation of that feature.
|
|
12
|
+
|
|
5
13
|
## 2.10.0 additions: compile-time components, subset fonts, hardening (2026-10-14)
|
|
6
14
|
|
|
7
15
|
- **Includes became components without leaving the macro model.** `defaults:` frontmatter gives an include's `with` arguments string fallbacks (`WD613` on a malformed array), and the `@include … @endinclude` block form compiles the enclosed lines once in the caller's scope and inlines them at the include's standalone `{ children }` line: the card/banner wrapper the vision always implied, at zero runtime cost. The pairing decision is deliberate and documented: `@endinclude` closes the nearest unpaired opener (an inline include in between consumes it), one level of block nesting is supported (`WD616` deeper), a stray closer is `WD614`, and there is NO unclosed-block error, because an unpaired opener is the inline form and every pre-feature page relies on it. `WD615` stays unassigned for a future unclosed-detection rule that avoids those false positives.
|
|
@@ -306,4 +314,4 @@ The compiler recognizes one directive, `:try`, that exists **only to power the d
|
|
|
306
314
|
|
|
307
315
|
Darkmown is faithful to the core thesis: Markdown remains the authoring center, static pages ship zero framework JavaScript, and reactive behavior compiles into tiny direct-DOM islands only where declared. The extension is the feature gate, the loop is one concept with progressive disclosure, and state scopes to document structure.
|
|
308
316
|
|
|
309
|
-
As of **v2.
|
|
317
|
+
As of **v2.10.0** the public npm package is published as `@zvndev/darkmown`, the project is MIT licensed, and the directive surface is stable at the 25 directives `darkmown catalog` prints. The releases that moved the surface are each recorded above: **2.0.0** made the one breaking change, escaping raw HTML in markdown by default (`html: false`), with `html: true` as the per-page opt-in, a security default flip rather than a directive change. **2.1.0** replaced the runtime's `new Function` with a closed interpreter over a validated expression AST, so reactive pages now run under the same strict, `'unsafe-eval'`-free CSP as static ones. **2.5.0** added the agent tool surface (`@zvndev/darkmown/tools`). **2.8.0** widened everything around the directive core (`skins:` shared skins, `@font-face`/`@keyframes` in `.skin`, `$build` bindings, format pipes in raw HTML) without adding a directive, **2.9.0** moved responsive image variants into the build, and **2.10.0** grew includes into the component model (`defaults:` arguments and the `@include` … `@endinclude` block form with a `{ children }` slot) plus build-time font subsetting, again with no new directive and no runtime change. The remaining items above stay scoped as post-launch product and distribution work rather than missing core claims.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"format:check": "biome format .",
|
|
50
50
|
"size:check": "node scripts/size-check.mjs",
|
|
51
51
|
"prepare": "husky || true",
|
|
52
|
-
"prepublish:check": "npm run typecheck && node scripts/build-runtime.mjs --check && npm run test:cov && npm run build && npm run runtime:size && npm audit --audit-level=high && npm run smoke",
|
|
52
|
+
"prepublish:check": "npm run typecheck && node scripts/build-runtime.mjs --check && node scripts/build-playground.mjs --check && npm run test:cov && npm run build && npm run runtime:size && npm audit --audit-level=high && npm run smoke",
|
|
53
53
|
"prepublishOnly": "npm run prepublish:check",
|
|
54
54
|
"og": "node scripts/og-image.mjs",
|
|
55
55
|
"bench:build": "node scripts/bench-build.mjs"
|
|
@@ -102,7 +102,7 @@ Measured on the same machine, 67 routes on both sides:
|
|
|
102
102
|
|
|
103
103
|
Both of these were real losses on measured bytes, not hypotheticals, and both shaped what shipped next.
|
|
104
104
|
|
|
105
|
-
- **Fonts, badly.** The rebuild served three raw variable TTFs: 1.07 MB on the homepage, against `next/font`'s 158 KB of subset woff2. Fonts were the single largest byte category on every rebuilt page. The 2.8 documentation
|
|
105
|
+
- **Fonts, badly.** The rebuild served three raw variable TTFs: 1.07 MB on the homepage, against `next/font`'s 158 KB of subset woff2. Fonts were the single largest byte category on every rebuilt page. The 2.8 documentation shipped a font recipe (convert to woff2, subset to the glyphs used, `font-display swap`, preload the critical face), and 2.10 closes the gap outright: with the optional toolchain installed, the build subsets project-local fonts to the site's charset automatically. Measured on these same demo fonts, the 1.07 MB of TTFs became 288 KB of woff2.
|
|
106
106
|
- **Photos.** The doctors page shipped 1,045 KB of headshots against the live site's 147 KB of CDN width variants. Darkmown 2.8 added the `@width` convention (drop in `photo@640.jpg` and the build wires the `srcset`), and 2.9 generates those variants at build time when sharp is installed. Run the same rebuild today and this category flips.
|
|
107
107
|
- **A shared cost neither side avoids:** both sites load the same ~594 KB hero video on the homepage. No edge for anyone there.
|
|
108
108
|
|