@zvndev/darkmown 2.10.0 → 2.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/AGENTS.md +8 -6
- package/CHANGELOG.md +23 -0
- package/README.md +37 -3
- package/SECURITY.md +1 -0
- package/docs/cli.md +20 -1
- package/docs/errors.md +255 -0
- package/docs/spec-alignment.md +14 -1
- package/package.json +3 -2
- package/site/pages/benchmarks/index.wd +1 -1
- package/site/pages/docs/index.wd +6 -1
- package/site/pages/index.wd +1 -0
- package/site/pages/playground/index.js +59 -54
- package/src/api-runner.js +80 -8
- package/src/builder.js +122 -13
- package/src/catalog.js +6 -0
- package/src/cli.js +48 -5
- package/src/compiler/page.js +16 -4
- package/src/compiler/state.js +75 -0
- package/src/compiler.js +1 -1
- 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/mcp-tools.js +354 -0
- package/src/mcp.js +287 -0
- package/src/router.js +16 -0
- package/src/skin.js +111 -45
- package/src/statics.js +1 -0
- 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/builder.d.ts +5 -0
- package/types/compiler/page.d.ts +12 -3
- package/types/compiler.d.ts +4 -1
- 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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[
|
|
2
|
-
{ "name": "Ceramic Mug", "
|
|
3
|
-
{ "name": "Linen Tote", "
|
|
4
|
-
{ "name": "Pocket Notebook", "
|
|
5
|
-
{ "name": "Desk Lamp", "
|
|
6
|
-
{ "name": "Plant Pot", "
|
|
7
|
-
{ "name": "Wall Clock", "
|
|
2
|
+
{ "name": "Ceramic Mug", "initial": "C", "price": 18 },
|
|
3
|
+
{ "name": "Linen Tote", "initial": "L", "price": 32 },
|
|
4
|
+
{ "name": "Pocket Notebook", "initial": "P", "price": 12 },
|
|
5
|
+
{ "name": "Desk Lamp", "initial": "D", "price": 64 },
|
|
6
|
+
{ "name": "Plant Pot", "initial": "P", "price": 24 },
|
|
7
|
+
{ "name": "Wall Clock", "initial": "W", "price": 48 }
|
|
8
8
|
]
|
|
@@ -93,13 +93,28 @@ input:focus-visible, select:focus-visible, textarea:focus-visible
|
|
|
93
93
|
border-color $accent
|
|
94
94
|
box-shadow 0 12px 26px -16px rgba(26,31,43,.4)
|
|
95
95
|
|
|
96
|
-
.
|
|
97
|
-
|
|
96
|
+
.tile
|
|
97
|
+
width 3rem
|
|
98
|
+
height 3rem
|
|
99
|
+
margin 0 auto
|
|
100
|
+
display grid
|
|
101
|
+
place-items center
|
|
102
|
+
font-size 1.3rem
|
|
103
|
+
font-weight 700
|
|
104
|
+
color $accent-deep
|
|
105
|
+
bg $paper
|
|
106
|
+
border 1px solid $line
|
|
107
|
+
radius 10px
|
|
98
108
|
|
|
99
109
|
.pname
|
|
100
110
|
font-weight 600
|
|
101
111
|
margin .35rem 0 .15rem
|
|
102
112
|
|
|
113
|
+
.note
|
|
114
|
+
color $muted
|
|
115
|
+
font-size .82rem
|
|
116
|
+
margin-bottom .6rem
|
|
117
|
+
|
|
103
118
|
.price
|
|
104
119
|
color $accent
|
|
105
120
|
font-weight 600
|
|
@@ -23,18 +23,9 @@ transitions: true
|
|
|
23
23
|
|
|
24
24
|
::: catalog
|
|
25
25
|
@loop /products.json into p where p.name contains q
|
|
26
|
-
|
|
27
|
-
::: emoji
|
|
28
|
-
{ p.emoji }
|
|
29
|
-
:::
|
|
30
|
-
::: pname
|
|
31
|
-
{ p.name }
|
|
32
|
-
:::
|
|
33
|
-
::: price
|
|
34
|
-
{ p.price | money }
|
|
35
|
-
:::
|
|
26
|
+
@include /card.wd
|
|
36
27
|
:button "Add to cart" -> cart += p
|
|
37
|
-
|
|
28
|
+
@endinclude
|
|
38
29
|
@empty
|
|
39
30
|
::: card .empty
|
|
40
31
|
Nothing matches "**{ q }**".
|
|
@@ -46,7 +37,7 @@ Nothing matches "**{ q }**".
|
|
|
46
37
|
|
|
47
38
|
@loop cart into item
|
|
48
39
|
::: card .line
|
|
49
|
-
|
|
40
|
+
**{ item.name }** — { item.price | money }
|
|
50
41
|
:button "Remove" -> cart remove item
|
|
51
42
|
:::
|
|
52
43
|
@empty
|
package/types/api-runner.d.ts
CHANGED
|
@@ -38,18 +38,25 @@ export function discoverApiRoutes(apiDir: string): ApiRoute[];
|
|
|
38
38
|
/**
|
|
39
39
|
* Match a request pathname against discovered routes. The pathname is expected
|
|
40
40
|
* to start with `/api`; anything else returns `null` (caller serves static).
|
|
41
|
+
* A dynamic segment whose percent-encoding is malformed (`%E0%A4%A`) makes
|
|
42
|
+
* `decodeURIComponent` throw — that is reported as `{ malformed: true }` so the
|
|
43
|
+
* caller answers a clean 400 instead of crashing the request handler.
|
|
41
44
|
* @param {ApiRoute[]} routes
|
|
42
45
|
* @param {string} pathname Request pathname (no query string).
|
|
43
|
-
* @returns {{ file: string, params: Record<string, string> } | null}
|
|
46
|
+
* @returns {{ file: string, params: Record<string, string> } | { malformed: true } | null}
|
|
44
47
|
*/
|
|
45
48
|
export function matchApiRoute(routes: ApiRoute[], pathname: string): {
|
|
46
49
|
file: string;
|
|
47
50
|
params: Record<string, string>;
|
|
51
|
+
} | {
|
|
52
|
+
malformed: true;
|
|
48
53
|
} | null;
|
|
49
54
|
/**
|
|
50
55
|
* Build a Web-standard `Request` from a Node `IncomingMessage`. The body (for
|
|
51
|
-
* methods that carry one) is buffered
|
|
52
|
-
* avoids the `duplex: "half"` streaming
|
|
56
|
+
* methods that carry one) is buffered up to {@link MAX_BODY_BYTES} — dev and
|
|
57
|
+
* preview endpoints are small and this avoids the `duplex: "half"` streaming
|
|
58
|
+
* dance. A larger body rejects with {@link BodyTooLargeError}, which
|
|
59
|
+
* `handleApiRequest` answers as a 413.
|
|
53
60
|
* @param {import("node:http").IncomingMessage} req
|
|
54
61
|
* @returns {Promise<Request>}
|
|
55
62
|
*/
|
|
@@ -75,19 +82,28 @@ export function writeWebResponse(response: Response, res: import("node:http").Se
|
|
|
75
82
|
* the handler succeeded or threw), `false` when nothing matched so the caller
|
|
76
83
|
* should fall through to static serving.
|
|
77
84
|
*
|
|
85
|
+
* Error disclosure: a handler throw is answered as JSON either way, but the
|
|
86
|
+
* `error` field carries the real message only when `discloseErrors` is set
|
|
87
|
+
* (the dev server's default — a documented dev convenience). The preview
|
|
88
|
+
* server passes `false` and the client gets the generic "Internal error",
|
|
89
|
+
* matching the shape the generated Cloudflare worker ships.
|
|
90
|
+
*
|
|
78
91
|
* @param {object} io
|
|
79
92
|
* @param {string} io.apiDir Absolute path to the project's `api/` directory.
|
|
80
93
|
* @param {import("node:http").IncomingMessage} io.req
|
|
81
94
|
* @param {import("node:http").ServerResponse} io.res
|
|
82
95
|
* @param {string | string[] | undefined} [io.acceptEncoding] The request's
|
|
83
96
|
* Accept-Encoding header, when the caller negotiates compression (preview).
|
|
97
|
+
* @param {boolean} [io.discloseErrors] Echo the thrown message to the client
|
|
98
|
+
* (default true, the dev behavior). Pass false on any production-shaped path.
|
|
84
99
|
* @returns {Promise<boolean>}
|
|
85
100
|
*/
|
|
86
|
-
export function handleApiRequest({ apiDir, req, res, acceptEncoding }: {
|
|
101
|
+
export function handleApiRequest({ apiDir, req, res, acceptEncoding, discloseErrors }: {
|
|
87
102
|
apiDir: string;
|
|
88
103
|
req: import("node:http").IncomingMessage;
|
|
89
104
|
res: import("node:http").ServerResponse;
|
|
90
105
|
acceptEncoding?: string | string[] | undefined;
|
|
106
|
+
discloseErrors?: boolean | undefined;
|
|
91
107
|
}): Promise<boolean>;
|
|
92
108
|
/**
|
|
93
109
|
* A discovered API route.
|
package/types/builder.d.ts
CHANGED
|
@@ -169,6 +169,11 @@ export type SiteIdentity = {
|
|
|
169
169
|
* Most-recent-first RSS item cap, from `rss_limit:`.
|
|
170
170
|
*/
|
|
171
171
|
rssLimit: number;
|
|
172
|
+
/**
|
|
173
|
+
* Publish every page's markdown source at
|
|
174
|
+
* `<route>.md`, from the home page's `md_source: true` (2.11).
|
|
175
|
+
*/
|
|
176
|
+
mdSource: boolean;
|
|
172
177
|
};
|
|
173
178
|
export type Paths = import("./config.js").Paths;
|
|
174
179
|
export type Assets = import("./compiler.js").Assets;
|
package/types/compiler/page.d.ts
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Compile a page source file into a full HTML document plus its assets.
|
|
3
3
|
* @param {string} file Absolute path to the source `.md`/`.wd` file.
|
|
4
4
|
* @param {Paths} context Resolved project paths.
|
|
5
|
-
* @param {{ feed?: { href: string, title: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./reader.js").Reader, imageVariants?: Map<string, number[]> | null }} [options]
|
|
5
|
+
* @param {{ feed?: { href: string, title: string }, mdSource?: { href: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./reader.js").Reader, imageVariants?: Map<string, number[]> | null }} [options]
|
|
6
6
|
* When a site-wide RSS feed is emitted (the home page set `site_url` and the
|
|
7
7
|
* site has dated posts), `feed` carries its absolute href + title so every page
|
|
8
|
-
* links it.
|
|
8
|
+
* links it. When the site publishes markdown sources (the home page set
|
|
9
|
+
* `md_source: true` and this page did not opt out), `mdSource` carries the
|
|
10
|
+
* page's own `<route>.md` href for the alternate link. `site` carries the
|
|
11
|
+
* origin + this page's own route (and its resolved
|
|
9
12
|
* breadcrumb trail) so the shell can state a canonical URL. `collections` is the
|
|
10
13
|
* build-time collection index a bare-name `@loop` resolves against; `vars` seeds
|
|
11
14
|
* the document scope (the pager `page` object on a paginated route). `reader` is
|
|
@@ -19,6 +22,9 @@ export function compilePage(file: string, context: Paths, options?: {
|
|
|
19
22
|
href: string;
|
|
20
23
|
title: string;
|
|
21
24
|
};
|
|
25
|
+
mdSource?: {
|
|
26
|
+
href: string;
|
|
27
|
+
};
|
|
22
28
|
site?: SiteContext;
|
|
23
29
|
collections?: Map<string, import("./collections.js").CollectionRow[]>;
|
|
24
30
|
vars?: Record<string, unknown>;
|
|
@@ -103,7 +109,7 @@ export function foldBuildBindings(text: string, now?: Date): string;
|
|
|
103
109
|
* @param {Record<string, string> | Map<string, string>} files Project-relative
|
|
104
110
|
* path → source content.
|
|
105
111
|
* @param {string} entryPath Project-relative path of the page to compile.
|
|
106
|
-
* @param {{ feed?: { href: string, title: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, cwd?: string }} [options]
|
|
112
|
+
* @param {{ feed?: { href: string, title: string }, mdSource?: { href: string }, site?: SiteContext, collections?: Map<string, import("./collections.js").CollectionRow[]>, vars?: Record<string, unknown>, cwd?: string }} [options]
|
|
107
113
|
* Same shell options as {@link compilePage}; `cwd` overrides the virtual
|
|
108
114
|
* project root the relative keys resolve against (defaults to `/`).
|
|
109
115
|
* @returns {CompiledPage}
|
|
@@ -113,6 +119,9 @@ export function compileFromMemory(files: Record<string, string> | Map<string, st
|
|
|
113
119
|
href: string;
|
|
114
120
|
title: string;
|
|
115
121
|
};
|
|
122
|
+
mdSource?: {
|
|
123
|
+
href: string;
|
|
124
|
+
};
|
|
116
125
|
site?: SiteContext;
|
|
117
126
|
collections?: Map<string, import("./collections.js").CollectionRow[]>;
|
|
118
127
|
vars?: Record<string, unknown>;
|
package/types/compiler.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* a different source (see `compileFromMemory`).
|
|
5
5
|
* @param {string} file Absolute path to the source `.md`/`.wd` file.
|
|
6
6
|
* @param {import("./config.js").Paths} context Resolved project paths.
|
|
7
|
-
* @param {{ feed?: { href: string, title: string }, site?: import("./compiler/page.js").SiteContext, collections?: Map<string, import("./compiler/collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./compiler/reader.js").Reader, imageVariants?: Map<string, number[]> | null }} [options]
|
|
7
|
+
* @param {{ feed?: { href: string, title: string }, mdSource?: { href: string }, site?: import("./compiler/page.js").SiteContext, collections?: Map<string, import("./compiler/collections.js").CollectionRow[]>, vars?: Record<string, unknown>, reader?: import("./compiler/reader.js").Reader, imageVariants?: Map<string, number[]> | null }} [options]
|
|
8
8
|
* @returns {CompiledPage}
|
|
9
9
|
*/
|
|
10
10
|
export function compilePage(file: string, context: import("./config.js").Paths, options?: {
|
|
@@ -12,6 +12,9 @@ export function compilePage(file: string, context: import("./config.js").Paths,
|
|
|
12
12
|
href: string;
|
|
13
13
|
title: string;
|
|
14
14
|
};
|
|
15
|
+
mdSource?: {
|
|
16
|
+
href: string;
|
|
17
|
+
};
|
|
15
18
|
site?: import("./compiler/page.js").SiteContext;
|
|
16
19
|
collections?: Map<string, import("./compiler/collections.js").CollectionRow[]>;
|
|
17
20
|
vars?: Record<string, unknown>;
|
package/types/errors.d.ts
CHANGED
|
@@ -33,8 +33,9 @@ export function errorsMarkdown(): string;
|
|
|
33
33
|
* @property {string} title Short name for the mistake.
|
|
34
34
|
* @property {string} cause What the author wrote that triggers it.
|
|
35
35
|
* @property {string} fix The corrective action, in one line.
|
|
36
|
-
* @property {string} [example] A concrete, compilable line,
|
|
37
|
-
*
|
|
36
|
+
* @property {string} [example] A concrete, compilable line (or, for block
|
|
37
|
+
* constructs like the `.skin` at-rules, a minimal compilable block), sourced
|
|
38
|
+
* from the compiler's own `*_EXAMPLE` constants.
|
|
38
39
|
*/
|
|
39
40
|
/** The subsystem blocks, in code order. */
|
|
40
41
|
/** @type {ErrorArea[]} */
|
|
@@ -93,8 +94,9 @@ export type ErrorEntry = {
|
|
|
93
94
|
*/
|
|
94
95
|
fix: string;
|
|
95
96
|
/**
|
|
96
|
-
* A concrete, compilable line,
|
|
97
|
-
*
|
|
97
|
+
* A concrete, compilable line (or, for block
|
|
98
|
+
* constructs like the `.skin` at-rules, a minimal compilable block), sourced
|
|
99
|
+
* from the compiler's own `*_EXAMPLE` constants.
|
|
98
100
|
*/
|
|
99
101
|
example?: string | undefined;
|
|
100
102
|
};
|
package/types/font-subset.d.ts
CHANGED
|
@@ -29,17 +29,21 @@ export function subsetFonts(paths: Paths, options?: {
|
|
|
29
29
|
* build is synchronous; the wasm API is not), communicating through temp JSON
|
|
30
30
|
* files rather than stdio so a large payload never hits a pipe buffer. The
|
|
31
31
|
* worker always writes a results file — a missing or unparsable one means the
|
|
32
|
-
* child itself died,
|
|
33
|
-
* as
|
|
32
|
+
* child itself died (crashed, or killed by the spawn timeout), which is
|
|
33
|
+
* reported as `unavailable` (the build then behaves as if the toolchain were
|
|
34
|
+
* absent: a hint, no subsets, no failure).
|
|
34
35
|
* @param {{ charset: number[], jobs: SubsetJob[] }} payload
|
|
35
36
|
* @param {(cmd: string, args: string[], opts: object) => unknown} [spawn]
|
|
36
37
|
* Injectable for tests; the real one is `spawnSync`.
|
|
38
|
+
* @param {number} [timeoutMs] The worker is killed after this long (default
|
|
39
|
+
* two minutes), so a hung toolchain call cannot stall a build forever. The
|
|
40
|
+
* kill lands in the same `unavailable` path as any other dead child.
|
|
37
41
|
* @returns {SubsetResult}
|
|
38
42
|
*/
|
|
39
43
|
export function runSubsetJobs(payload: {
|
|
40
44
|
charset: number[];
|
|
41
45
|
jobs: SubsetJob[];
|
|
42
|
-
}, spawn?: (cmd: string, args: string[], opts: object) => unknown): SubsetResult;
|
|
46
|
+
}, spawn?: (cmd: string, args: string[], opts: object) => unknown, timeoutMs?: number): SubsetResult;
|
|
43
47
|
/**
|
|
44
48
|
* The subset charset for a build: the base set plus every code point appearing
|
|
45
49
|
* in the given texts (the emitted pages' HTML). Numeric character references
|
|
@@ -29,14 +29,18 @@ export function generateImageVariants(paths: Paths, options?: {
|
|
|
29
29
|
* build is synchronous; sharp is not), communicating through temp JSON files
|
|
30
30
|
* rather than stdio so a large job list never hits a pipe buffer. The worker
|
|
31
31
|
* always writes a results file — a missing or unparsable one means the child
|
|
32
|
-
* itself died,
|
|
33
|
-
* if sharp were absent: a hint, no
|
|
32
|
+
* itself died (crashed, or killed by the spawn timeout), which is reported as
|
|
33
|
+
* `unavailable` (the build then behaves as if sharp were absent: a hint, no
|
|
34
|
+
* variants, no failure).
|
|
34
35
|
* @param {ResizeJob[]} jobs
|
|
35
36
|
* @param {(cmd: string, args: string[], opts: object) => unknown} [spawn]
|
|
36
37
|
* Injectable for tests; the real one is `spawnSync`.
|
|
38
|
+
* @param {number} [timeoutMs] The worker is killed after this long (default
|
|
39
|
+
* two minutes), so a hung sharp call cannot stall a build forever. The kill
|
|
40
|
+
* lands in the same `unavailable` path as any other dead child.
|
|
37
41
|
* @returns {ResizeResult}
|
|
38
42
|
*/
|
|
39
|
-
export function runResizeJobs(jobs: ResizeJob[], spawn?: (cmd: string, args: string[], opts: object) => unknown): ResizeResult;
|
|
43
|
+
export function runResizeJobs(jobs: ResizeJob[], spawn?: (cmd: string, args: string[], opts: object) => unknown, timeoutMs?: number): ResizeResult;
|
|
40
44
|
/**
|
|
41
45
|
* @typedef {import("./config.js").Paths} Paths
|
|
42
46
|
*/
|
package/types/router.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export function discoverRoutes(routesRoot: string, options?: {
|
|
|
38
38
|
export function isDraft(meta: Meta): boolean;
|
|
39
39
|
/**
|
|
40
40
|
* Derive a public route path from a source file path.
|
|
41
|
+
*
|
|
42
|
+
* Fail-closed on control/whitespace characters: the route string flows RAW
|
|
43
|
+
* into generated config (`dist/_headers` blocks, llms.txt lines), where a
|
|
44
|
+
* newline in a filename would inject whole config lines. Rejecting here, at
|
|
45
|
+
* the one route-derivation chokepoint, protects every downstream sink at once.
|
|
41
46
|
* @param {string} routesRoot Absolute path to `site/pages`.
|
|
42
47
|
* @param {string} file Absolute path to the source file.
|
|
43
48
|
* @returns {string}
|
package/types/skin.d.ts
CHANGED
|
@@ -33,16 +33,21 @@ export function scopeIdFor(relPath: string): string;
|
|
|
33
33
|
* selectors), they are ALWAYS global (never scope-stamped, same contract as
|
|
34
34
|
* `tokens`), and their declaration values use the same alias/`$token` pipeline.
|
|
35
35
|
* @param {string} source
|
|
36
|
-
* @param {{ scope?: string, subjects?: Set<string
|
|
37
|
-
* id enabling scoped mode. `subjects`: an optional set the
|
|
38
|
-
* every scoped subject token (`.card`, `h2`, `#id`) for
|
|
39
|
-
* warning, so the caller need not re-parse the source.
|
|
36
|
+
* @param {{ scope?: string, subjects?: Set<string>, file?: string }} [opts]
|
|
37
|
+
* `scope`: the scope id enabling scoped mode. `subjects`: an optional set the
|
|
38
|
+
* compiler fills with every scoped subject token (`.card`, `h2`, `#id`) for
|
|
39
|
+
* the unused-selector warning, so the caller need not re-parse the source.
|
|
40
|
+
* `file`: the skin's path, threaded into author-facing errors so they report
|
|
41
|
+
* `file:line` (the builder always passes it; a bare call reports `on line N`).
|
|
40
42
|
* @returns {string}
|
|
41
43
|
*/
|
|
42
44
|
export function compileSkin(source: string, opts?: {
|
|
43
45
|
scope?: string;
|
|
44
46
|
subjects?: Set<string>;
|
|
47
|
+
file?: string;
|
|
45
48
|
}): string;
|
|
49
|
+
export const FONT_FACE_EXAMPLE: "@font-face\n font-family \"Newsreader\"";
|
|
50
|
+
export const KEYFRAMES_EXAMPLE: "@keyframes fade-in\n to\n opacity 1";
|
|
46
51
|
/**
|
|
47
52
|
* A nesting frame in the indentation stack.
|
|
48
53
|
*/
|