@zvndev/darkmown 2.9.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.
Files changed (54) hide show
  1. package/AGENTS.md +23 -12
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +105 -6
  4. package/SECURITY.md +1 -0
  5. package/docs/cli.md +1 -1
  6. package/docs/spec-alignment.md +16 -2
  7. package/grammar/wd-directives.gbnf +1 -1
  8. package/package.json +8 -4
  9. package/site/pages/benchmarks/index.skin +67 -0
  10. package/site/pages/benchmarks/index.wd +126 -0
  11. package/site/pages/docs/index.wd +47 -2
  12. package/site/pages/index.wd +1 -0
  13. package/site/pages/playground/index.js +58 -54
  14. package/src/api-runner.js +80 -8
  15. package/src/builder.js +70 -15
  16. package/src/catalog.js +13 -4
  17. package/src/cli.js +18 -6
  18. package/src/compiler/body.js +30 -1
  19. package/src/compiler/context.js +13 -1
  20. package/src/compiler/includes.js +44 -1
  21. package/src/compiler/media.js +25 -0
  22. package/src/compiler/page.js +6 -0
  23. package/src/compiler/state.js +75 -0
  24. package/src/compiler/structure.js +272 -4
  25. package/src/errors.js +74 -3
  26. package/src/font-subset-worker.js +131 -0
  27. package/src/font-subset.js +475 -0
  28. package/src/grammar.js +3 -2
  29. package/src/headers.js +28 -1
  30. package/src/image-variant-resize.js +4 -1
  31. package/src/image-variants.js +9 -4
  32. package/src/router.js +16 -0
  33. package/src/skin.js +111 -45
  34. package/src/statics.js +22 -14
  35. package/src/templates/dashboard/README.md +3 -1
  36. package/src/templates/landing/site/pages/index.skin +7 -0
  37. package/src/templates/landing/site/pages/index.wd +10 -3
  38. package/src/templates/store/README.md +5 -2
  39. package/src/templates/store/site/_/card.wd +19 -0
  40. package/src/templates/store/site/_/products.json +6 -6
  41. package/src/templates/store/site/pages/index.skin +17 -2
  42. package/src/templates/store/site/pages/index.wd +3 -12
  43. package/types/api-runner.d.ts +20 -4
  44. package/types/builder.d.ts +25 -1
  45. package/types/catalog.d.ts +3 -2
  46. package/types/compiler/context.d.ts +28 -0
  47. package/types/compiler/includes.d.ts +23 -0
  48. package/types/compiler/structure.d.ts +53 -1
  49. package/types/errors.d.ts +6 -4
  50. package/types/font-subset.d.ts +144 -0
  51. package/types/headers.d.ts +14 -0
  52. package/types/image-variants.d.ts +7 -3
  53. package/types/router.d.ts +5 -0
  54. package/types/skin.d.ts +9 -4
@@ -53,6 +53,12 @@ export function stripRuntimeComments(source: string): string;
53
53
  *
54
54
  * Clean URLs mean a route like `/docs/` is served at both `/docs` and `/docs/`,
55
55
  * so each static route emits a path glob (`/docs`, `/docs/*`) that covers both.
56
+ *
57
+ * A page whose `:embed`s frame hosts beyond the base policy (a booking widget,
58
+ * a map) carries those hosts on its manifest entry (`assets.embedHosts`), and
59
+ * its block — static or reactive — states the unioned per-route policy
60
+ * ({@link routeCsp}). A reactive route with embed hosts can no longer inherit
61
+ * the catch-all, so it always gets its own block.
56
62
  * @param {RouteManifestEntry[]} manifest
57
63
  * @returns {string}
58
64
  */
@@ -68,7 +74,10 @@ export function renderCloudflareHeaders(manifest: RouteManifestEntry[]): string;
68
74
  * @typedef {object} RouteManifestEntry
69
75
  * @property {string} route Public route path.
70
76
  * @property {string} file Source file path, relative to cwd, POSIX-separated.
71
- * @property {{ skins: string[], scripts: string[], runtime: boolean }} assets
77
+ * @property {{ skins: string[], scripts: string[], runtime: boolean, embedHosts?: string[] }} assets
78
+ * `embedHosts` is present only when the page's `:embed`s frame at least one
79
+ * source the base CSP does not already cover — absent keeps a no-embed
80
+ * site's routes.json byte-identical.
72
81
  */
73
82
  /**
74
83
  * One source route's record in the dev dependency map (`dist/.wd-dev-deps.json`),
@@ -107,6 +116,15 @@ export type BuildResult = {
107
116
  cached: number;
108
117
  unavailable: boolean;
109
118
  };
119
+ /**
120
+ * Font-subset post-pass counts (2.10.0): freshly subsetted, cache-served, and
121
+ * whether harfbuzzjs/wawoff2 could not be loaded at all.
122
+ */
123
+ fonts: {
124
+ subsetted: number;
125
+ cached: number;
126
+ unavailable: boolean;
127
+ };
110
128
  incremental?: {
111
129
  changed: string[];
112
130
  rebuilt: string[];
@@ -168,10 +186,16 @@ export type RouteManifestEntry = {
168
186
  * Source file path, relative to cwd, POSIX-separated.
169
187
  */
170
188
  file: string;
189
+ /**
190
+ * `embedHosts` is present only when the page's `:embed`s frame at least one
191
+ * source the base CSP does not already cover — absent keeps a no-embed
192
+ * site's routes.json byte-identical.
193
+ */
171
194
  assets: {
172
195
  skins: string[];
173
196
  scripts: string[];
174
197
  runtime: boolean;
198
+ embedHosts?: string[];
175
199
  };
176
200
  };
177
201
  /**
@@ -107,9 +107,10 @@ export type DirectiveEntry = {
107
107
  */
108
108
  name: string;
109
109
  /**
110
- * A single line, or an opener with a closer.
110
+ * A single line, an opener with a
111
+ * closer, or a line that may optionally open a block (`@include … @endinclude`).
111
112
  */
112
- kind: "line" | "block";
113
+ kind: "line" | "block" | "either";
113
114
  /**
114
115
  * One-line schematic (`[…]` = optional, `<…>` = slot).
115
116
  */
@@ -26,6 +26,12 @@
26
26
  * that opted into scoping (`scoped` first line). The builder emits these with
27
27
  * the scope attribute and runs the unused-selector warning against the stamped
28
28
  * subtree; the HTML stamp uses them to scope the matching page/include subtree.
29
+ * @property {Set<string>} embedHosts Frame sources (origins like
30
+ * `https://app.example.com`, or `'self'` for a same-origin embed) of every
31
+ * `:embed` on the page, collected AFTER the YouTube/Vimeo rewrite. The builder
32
+ * records them on the route manifest entry so the per-route CSP's `frame-src`
33
+ * authorizes exactly the hosts this page frames (see `routeCsp` in
34
+ * src/headers.js).
29
35
  */
30
36
  /**
31
37
  * The pagination intent a `@loop … paginate N` records during compile, so the
@@ -129,6 +135,11 @@
129
135
  * opener's `file:line` + raw text, set when descending into a loop body so the
130
136
  * reactive-nesting-depth error can point at the offending opener.
131
137
  * @property {boolean} [loopMeta] Inside a loop body, so `$index`/`$first`/… are valid.
138
+ * @property {number} [includeBlockDepth] Enclosing block-form `@include`
139
+ * CHILDREN levels (0 at page level, absent on a fresh file compile). Set
140
+ * while compiling a block's children so a block include nested past one
141
+ * level inside children is a compile error (WD616, together with the
142
+ * include-stack depth check for include bodies).
132
143
  * @property {import("markdown-it").default} [md] Markdown-it instance selected for this file.
133
144
  * @property {(lines: string[], ctx: Ctx) => string} compileBody The body
134
145
  * dispatcher, threaded through the context so block handlers (`:if`, `:::`,
@@ -410,6 +421,15 @@ export type Assets = {
410
421
  * subtree; the HTML stamp uses them to scope the matching page/include subtree.
411
422
  */
412
423
  scopedSkins: Set<string>;
424
+ /**
425
+ * Frame sources (origins like
426
+ * `https://app.example.com`, or `'self'` for a same-origin embed) of every
427
+ * `:embed` on the page, collected AFTER the YouTube/Vimeo rewrite. The builder
428
+ * records them on the route manifest entry so the per-route CSP's `frame-src`
429
+ * authorizes exactly the hosts this page frames (see `routeCsp` in
430
+ * src/headers.js).
431
+ */
432
+ embedHosts: Set<string>;
413
433
  };
414
434
  /**
415
435
  * The pagination intent a `@loop … paginate N` records during compile, so the
@@ -630,6 +650,14 @@ export type Ctx = {
630
650
  * Inside a loop body, so `$index`/`$first`/… are valid.
631
651
  */
632
652
  loopMeta?: boolean | undefined;
653
+ /**
654
+ * Enclosing block-form `@include`
655
+ * CHILDREN levels (0 at page level, absent on a fresh file compile). Set
656
+ * while compiling a block's children so a block include nested past one
657
+ * level inside children is a compile error (WD616, together with the
658
+ * include-stack depth check for include bodies).
659
+ */
660
+ includeBlockDepth?: number | undefined;
633
661
  /**
634
662
  * Markdown-it instance selected for this file.
635
663
  */
@@ -101,6 +101,15 @@ export function resolveInclude(spec: string, fromFile: string, context: Paths, a
101
101
  * @returns {boolean}
102
102
  */
103
103
  export function isAllowedInclude(file: string, context: Paths, reader?: import("./reader.js").Reader): boolean;
104
+ /**
105
+ * The compiled children HTML for the include currently compiling, or null when
106
+ * the file is not being compiled as an include at all (a top-level page) — in
107
+ * which case a `{ children }` line stays literal text. An include called
108
+ * WITHOUT a block carries "" (the slot renders empty).
109
+ * @param {import("./context.js").Ctx} ctx
110
+ * @returns {string | null}
111
+ */
112
+ export function childrenSlot(ctx: import("./context.js").Ctx): string | null;
104
113
  /**
105
114
  * Warn when a plain `.md` file contains `.wd`-only syntax that stays inert.
106
115
  * @param {string} body
@@ -109,6 +118,20 @@ export function isAllowedInclude(file: string, context: Paths, reader?: import("
109
118
  * @returns {void}
110
119
  */
111
120
  export function scanMarkdownHints(body: string, file: string, comp: Compilation): void;
121
+ /**
122
+ * Scope key carrying the compiled children HTML for the include currently
123
+ * being compiled. Control-character-prefixed so no author-visible name can
124
+ * collide with it or read it through `{ }` interpolation.
125
+ */
126
+ export const INCLUDE_CHILDREN: "\0children";
127
+ /**
128
+ * A line that is exactly the slot marker (`{ children }`, whitespace-tolerant),
129
+ * matching the spelling freedom of `{ name }` interpolation. Shared by the
130
+ * body parser (which splices the slot) and the include handler (which scans an
131
+ * include's body for whether the slot is ever used).
132
+ * @type {RegExp}
133
+ */
134
+ export const CHILDREN_SLOT_LINE: RegExp;
112
135
  export type Paths = import("./context.js").Paths;
113
136
  export type Assets = import("./context.js").Assets;
114
137
  export type Compilation = import("./context.js").Compilation;
@@ -24,12 +24,64 @@ export function takeA11yAttrs(rest: string, ctx: Ctx, index: number, what: strin
24
24
  */
25
25
  export function a11yAttrHtml(attrs: [string, string][]): string;
26
26
  /**
27
+ * Compile an `@include` line — inline form (`@include /nav.wd`) or block form
28
+ * (`@include /card.wd … @endinclude`, with `childrenLines` holding the lines
29
+ * between opener and closer, captured by {@link scanIncludeBlock}).
30
+ *
31
+ * Block form is compile-time macro expansion and nothing more: the children
32
+ * lines compile in the CALLER's context (its scope, its `:state`, its loop
33
+ * item when the call sits in a `@loop` body) BEFORE the target compiles, and
34
+ * the resulting HTML rides into the include's scope under
35
+ * {@link INCLUDE_CHILDREN}, where the body parser splices it at each
36
+ * `{ children }` slot line. No runtime marker, no reactivity of its own.
37
+ * Children compile ONCE even when the include places the slot twice, so state
38
+ * a child line declares is declared once in the caller.
39
+ *
40
+ * The include file's own frontmatter may declare `defaults: [name="value" …]`
41
+ * fallback values for `with` arguments (see {@link parseIncludeDefaults}); a
42
+ * caller-provided argument always wins over a default, and an argument with
43
+ * neither keeps the literal-braces behavior.
27
44
  * @param {string} line
28
45
  * @param {Ctx} ctx
29
46
  * @param {number} index 0-based line index for `file:line` errors.
47
+ * @param {string[]} [childrenLines] Lines between `@include` and `@endinclude`
48
+ * for the block form; undefined for the inline form.
30
49
  * @returns {string}
31
50
  */
32
- export function handleInclude(line: string, ctx: Ctx, index: number): string;
51
+ export function handleInclude(line: string, ctx: Ctx, index: number, childrenLines?: string[]): string;
52
+ /**
53
+ * Scan ahead from an `@include` opener for the `@endinclude` that pairs with
54
+ * it. Pairing is nearest-opener-first: a nested `@include` line pushes, an
55
+ * `@endinclude` line pops the most recent unclosed opener, and the first
56
+ * `@endinclude` found with nothing on the stack belongs to THIS opener — so
57
+ * nested blocks pair correctly, and an inline `@include` earlier in the file
58
+ * than a block (the common nav-include-then-card-block page) is unaffected.
59
+ * Fenced code is skipped, exactly as in the other block scanners.
60
+ *
61
+ * Returns null when no closer pairs — the include is the INLINE form, which
62
+ * stays legal (backward compatible): an opener that never pairs is an inline
63
+ * include followed by ordinary prose, and the two are indistinguishable, so
64
+ * there is deliberately no "unclosed block" error.
65
+ * @param {string[]} lines The current body slice.
66
+ * @param {number} start 0-based index of the `@include` opener line.
67
+ * @returns {{ body: string[], end: number } | null} The children lines and the
68
+ * closer's 0-based index, or null for the inline form.
69
+ */
70
+ export function scanIncludeBlock(lines: string[], start: number): {
71
+ body: string[];
72
+ end: number;
73
+ } | null;
74
+ /**
75
+ * `@endinclude` reached the body dispatcher, meaning no `@include` opener
76
+ * claimed it — the one-closer-too-many case (a typo'd closer, or a nested
77
+ * block attempted inside a children block, where the inner opener already
78
+ * took the closer the author meant for the outer one). Always throws.
79
+ * @param {string} line The offending line, for the message.
80
+ * @param {Ctx} ctx
81
+ * @param {number} index 0-based line index for `file:line`.
82
+ * @returns {never}
83
+ */
84
+ export function strayEndinclude(line: string, ctx: Ctx, index: number): never;
33
85
  /**
34
86
  * @param {string} header
35
87
  * @param {string[]} bodyLines
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, sourced from the
37
- * compiler's own `*_EXAMPLE` constants.
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, sourced from the
97
- * compiler's own `*_EXAMPLE` constants.
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
  };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Subset every project-local font referenced by the emitted stylesheets and
3
+ * rewrite the dist CSS to lead with the subset woff2.
4
+ *
5
+ * Skips, none of them errors: remote/protocol-relative/data/relative URLs
6
+ * (silently), already-woff2 sources (silently), URLs that escape or miss the
7
+ * project (silently — a broken link is an authoring bug, not a subsetting one).
8
+ * An unreadable font file, and one the toolchain fails on, get a hint naming
9
+ * the file and ship as-is. With no `@font-face` in any emitted stylesheet the
10
+ * pass is a no-op and never even scans the HTML.
11
+ * @param {Paths} paths
12
+ * @param {{ cacheDir?: string, quiet?: boolean,
13
+ * runJobs?: (payload: { charset: number[], jobs: SubsetJob[] }) => SubsetResult }} [options]
14
+ * `quiet` suppresses the hints (dev-server rebuilds). `runJobs` is the subset
15
+ * seam — the default spawns the worker process; tests inject fakes for the
16
+ * unavailable/failure paths.
17
+ * @returns {FontSubsetPlan}
18
+ */
19
+ export function subsetFonts(paths: Paths, options?: {
20
+ cacheDir?: string;
21
+ quiet?: boolean;
22
+ runJobs?: (payload: {
23
+ charset: number[];
24
+ jobs: SubsetJob[];
25
+ }) => SubsetResult;
26
+ }): FontSubsetPlan;
27
+ /**
28
+ * The default subset seam: run every pending job in ONE child process (the
29
+ * build is synchronous; the wasm API is not), communicating through temp JSON
30
+ * files rather than stdio so a large payload never hits a pipe buffer. The
31
+ * worker always writes a results file — a missing or unparsable one means the
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).
35
+ * @param {{ charset: number[], jobs: SubsetJob[] }} payload
36
+ * @param {(cmd: string, args: string[], opts: object) => unknown} [spawn]
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.
41
+ * @returns {SubsetResult}
42
+ */
43
+ export function runSubsetJobs(payload: {
44
+ charset: number[];
45
+ jobs: SubsetJob[];
46
+ }, spawn?: (cmd: string, args: string[], opts: object) => unknown, timeoutMs?: number): SubsetResult;
47
+ /**
48
+ * The subset charset for a build: the base set plus every code point appearing
49
+ * in the given texts (the emitted pages' HTML). Numeric character references
50
+ * (`&#9733;`, `&#x2605;`) are decoded first so a character the compiler emitted
51
+ * as an entity still lands in the union. Tags and attribute syntax contribute
52
+ * only ASCII, which the base set already carries, so the whole-file scan can
53
+ * only ever over-include by a handful of glyphs.
54
+ * @param {string[]} texts
55
+ * @returns {number[]} Sorted ascending, deduped.
56
+ */
57
+ export function charsetFromTexts(texts: string[]): number[];
58
+ /**
59
+ * Rewrite the `@font-face` blocks of an emitted stylesheet so each subsetted
60
+ * font's `src` leads with the subset woff2, the original kept as fallback:
61
+ * src: url("/f/a-subset.woff2") format("woff2"), url("/f/a.ttf") format("truetype");
62
+ * Only the `src` declaration changes — font-display and every other descriptor
63
+ * pass through byte-identically. Idempotent: a block already carrying the
64
+ * subset reference (a previous build's rewrite of a since-unedited skin) is
65
+ * left alone.
66
+ * @param {string} css Emitted stylesheet contents.
67
+ * @param {Map<string, string>} subsetByUrl url() target as written → subset URL.
68
+ * @returns {string}
69
+ */
70
+ export function rewriteFontFaceCss(css: string, subsetByUrl: Map<string, string>): string;
71
+ /** The base charset, sorted ascending. */
72
+ export const BASE_CODEPOINTS: number[];
73
+ export type Paths = import("./config.js").Paths;
74
+ /**
75
+ * One subset handed to the worker: reduce `source` to the shared charset and
76
+ * write the woff2 result to `out`, the cache path.
77
+ */
78
+ export type SubsetJob = {
79
+ /**
80
+ * Absolute path of the original font file.
81
+ */
82
+ source: string;
83
+ /**
84
+ * Absolute cache path the subset woff2 is written to.
85
+ */
86
+ out: string;
87
+ };
88
+ /**
89
+ * What the worker reports back. `unavailable` means the toolchain could not be
90
+ * loaded at all (harfbuzzjs/wawoff2 not installed) — every pending font then
91
+ * ships unsubsetted. `failed` carries one entry per font the toolchain itself
92
+ * rejected (corrupt payload, unsupported format).
93
+ */
94
+ export type SubsetResult = {
95
+ unavailable: boolean;
96
+ failed: {
97
+ file: string;
98
+ error: string;
99
+ }[];
100
+ };
101
+ /**
102
+ * The post-pass result, feeding the build summary. `unavailable` marks the
103
+ * toolchain-absent build (which is otherwise identical to not having the
104
+ * feature).
105
+ */
106
+ export type FontSubsetPlan = {
107
+ /**
108
+ * Fonts freshly subsetted this build.
109
+ */
110
+ subsetted: number;
111
+ /**
112
+ * Fonts served from the on-disk cache.
113
+ */
114
+ cached: number;
115
+ /**
116
+ * harfbuzzjs/wawoff2 could not be loaded.
117
+ */
118
+ unavailable: boolean;
119
+ };
120
+ /**
121
+ * A project-local font referenced by an emitted `@font-face` rule.
122
+ */
123
+ export type FontRef = {
124
+ /**
125
+ * The url() target exactly as written in the CSS.
126
+ */
127
+ url: string;
128
+ /**
129
+ * Absolute path of the font file it resolves to.
130
+ */
131
+ source: string;
132
+ /**
133
+ * Served URL of the subset (`<stem>-subset.woff2`).
134
+ */
135
+ subsetUrl: string;
136
+ /**
137
+ * Absolute dist path the subset is written to.
138
+ */
139
+ subsetDist: string;
140
+ /**
141
+ * Absolute cache path, set once the hashes are known.
142
+ */
143
+ cacheFile?: string | undefined;
144
+ };
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The CSP for one route: its base policy (`REACTIVE_CSP`/`STATIC_CSP`) with the
3
+ * page's `:embed` frame hosts unioned into `frame-src`. The compiler records the
4
+ * FINAL iframe src of every `:embed` (after the YouTube/Vimeo rewrite), so a
5
+ * generic embed — a booking widget, a map — is authorized on exactly the pages
6
+ * that carry it instead of being blocked by the shipped policy. YouTube/Vimeo
7
+ * embeds dedupe against the base list, so a page whose embeds are all covered
8
+ * already produces a byte-identical policy, as does a page with no embeds.
9
+ * @param {boolean} reactive Whether the route ships the reactive runtime.
10
+ * @param {Iterable<string>} [embedHosts] Origins (`https://app.example.com`) or
11
+ * `'self'` for a same-origin embed, collected at compile time.
12
+ * @returns {string}
13
+ */
14
+ export function routeCsp(reactive: boolean, embedHosts?: Iterable<string>): string;
1
15
  /**
2
16
  * The full security-header set for one response, including the CSP variant.
3
17
  * @param {boolean} reactive Whether the route ships the reactive runtime.
@@ -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, which is reported as `unavailable` (the build then behaves as
33
- * if sharp were absent: a hint, no variants, no failure).
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> }} [opts] `scope`: the scope
37
- * id enabling scoped mode. `subjects`: an optional set the compiler fills with
38
- * every scoped subject token (`.card`, `h2`, `#id`) for the unused-selector
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
  */