@zvndev/darkmown 2.1.0 → 2.3.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 (74) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +65 -0
  3. package/docs/cli.md +8 -0
  4. package/docs/spec-alignment.md +18 -0
  5. package/grammar/wd-directives.gbnf +58 -0
  6. package/package.json +17 -10
  7. package/site/pages/docs/index.wd +29 -0
  8. package/site/pages/playground/index.js +47 -46
  9. package/src/builder.js +5 -0
  10. package/src/catalog.js +601 -0
  11. package/src/cli.js +8 -0
  12. package/src/compiler/actions.js +87 -34
  13. package/src/compiler/body.js +36 -14
  14. package/src/compiler/collections.js +16 -10
  15. package/src/compiler/context.js +70 -1
  16. package/src/compiler/fetch.js +51 -19
  17. package/src/compiler/format.js +5 -2
  18. package/src/compiler/forms.js +169 -50
  19. package/src/compiler/frontmatter.js +11 -2
  20. package/src/compiler/fs-reader.js +2 -1
  21. package/src/compiler/image-size.js +340 -0
  22. package/src/compiler/includes.js +8 -4
  23. package/src/compiler/interpolation.js +15 -7
  24. package/src/compiler/loops.js +78 -39
  25. package/src/compiler/markdown.js +4 -3
  26. package/src/compiler/media.js +38 -10
  27. package/src/compiler/page.js +8 -6
  28. package/src/compiler/predicates.js +74 -30
  29. package/src/compiler/reader.js +12 -2
  30. package/src/compiler/state.js +83 -18
  31. package/src/compiler/structure.js +57 -24
  32. package/src/deploy.js +8 -4
  33. package/src/errors.js +1156 -0
  34. package/src/grammar.js +131 -0
  35. package/src/index.js +1 -0
  36. package/src/router.js +7 -4
  37. package/src/scaffold.js +4 -2
  38. package/src/skin.js +17 -6
  39. package/types/api-runner.d.ts +94 -0
  40. package/types/builder.d.ts +141 -10
  41. package/types/catalog.d.ts +97 -0
  42. package/types/compiler/actions.d.ts +41 -0
  43. package/types/compiler/body.d.ts +11 -0
  44. package/types/compiler/collections.d.ts +76 -0
  45. package/types/compiler/context.d.ts +661 -0
  46. package/types/compiler/directives.d.ts +6 -0
  47. package/types/compiler/expr-ast.d.ts +20 -0
  48. package/types/compiler/fetch.d.ts +29 -0
  49. package/types/compiler/format.d.ts +49 -0
  50. package/types/compiler/forms.d.ts +94 -0
  51. package/types/compiler/frontmatter.d.ts +33 -0
  52. package/types/compiler/fs-reader.d.ts +7 -0
  53. package/types/compiler/image-size.d.ts +14 -0
  54. package/types/compiler/includes.d.ts +100 -0
  55. package/types/compiler/index.d.ts +5 -0
  56. package/types/compiler/interpolation.d.ts +115 -0
  57. package/types/compiler/loops.d.ts +36 -0
  58. package/types/compiler/markdown.d.ts +34 -0
  59. package/types/compiler/media.d.ts +32 -0
  60. package/types/compiler/page.d.ts +109 -0
  61. package/types/compiler/predicates.d.ts +50 -0
  62. package/types/compiler/reader.d.ts +56 -0
  63. package/types/compiler/state.d.ts +68 -0
  64. package/types/compiler/structure.d.ts +56 -0
  65. package/types/compiler.d.ts +35 -223
  66. package/types/dev.d.ts +47 -2
  67. package/types/errors.d.ts +100 -0
  68. package/types/feeds.d.ts +150 -0
  69. package/types/headers.d.ts +44 -0
  70. package/types/highlight.d.ts +24 -0
  71. package/types/router.d.ts +42 -3
  72. package/types/scaffold.d.ts +15 -3
  73. package/types/skin.d.ts +28 -1
  74. package/types/statics.d.ts +2 -1
@@ -13,13 +13,18 @@
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
15
  import path from "node:path";
16
+ import { normalizeNewlines } from "./context.js";
16
17
 
17
18
  /**
18
19
  * The narrow filesystem surface the compiler needs. Every method takes an
19
20
  * ABSOLUTE path (the compiler resolves includes/assets to absolute paths before
20
21
  * reading), so a reader only has to map absolute paths to bytes/existence.
21
22
  * @typedef {object} Reader
22
- * @property {(absPath: string) => string} readText Read a UTF-8 text file.
23
+ * @property {(absPath: string) => string} readText Read a UTF-8 text file. The
24
+ * result is ALWAYS LF-normalized: every parser downstream is line-structured
25
+ * and LF-shaped, so a CRLF source (Windows, or any `core.autocrlf` checkout)
26
+ * would otherwise fail to match delimiters like `---`. A custom reader must
27
+ * uphold this.
23
28
  * @property {(absPath: string) => Uint8Array} readBinary Read raw bytes (images).
24
29
  * @property {(absPath: string) => boolean} exists Whether the path exists.
25
30
  * @property {(absPath: string) => string} realpath Canonical path (symlinks
@@ -40,6 +45,11 @@ import path from "node:path";
40
45
  */
41
46
  export function memoryReader(files, cwd = "/") {
42
47
  const map = files instanceof Map ? files : new Map(Object.entries(files));
48
+ // Resolution deliberately uses the HOST `path`, because `cwd` is frequently a
49
+ // real OS directory (the fs-parity test compiles the same project from disk
50
+ // and from memory against one temp root, and a Windows temp root is
51
+ // `C:\…` — which `path.posix` does not recognize as absolute). Only the
52
+ // resulting KEY is forced to POSIX, matching the map's documented key shape.
43
53
  const keyOf = (/** @type {string} */ absPath) =>
44
54
  path.relative(cwd, path.resolve(absPath)).split(path.sep).join("/");
45
55
  return {
@@ -49,7 +59,7 @@ export function memoryReader(files, cwd = "/") {
49
59
  if (value === undefined) {
50
60
  throw new Error(`ENOENT: no in-memory file "${key}" (from ${absPath})`);
51
61
  }
52
- return value;
62
+ return normalizeNewlines(value);
53
63
  },
54
64
  readBinary: () => {
55
65
  throw new Error("memoryReader: binary reads are unsupported");
@@ -8,7 +8,7 @@
8
8
  // while keeping the message text and `Use:` hints intact.
9
9
  // ---------------------------------------------------------------------------
10
10
 
11
- import { at } from "./context.js";
11
+ import { at, lineOf, wdError } from "./context.js";
12
12
  import { astOf, evalAst } from "./expr-ast.js";
13
13
  import { escapeHtml, parseStateValue, safeScriptJson } from "./interpolation.js";
14
14
  import { compileComputedExpr } from "./predicates.js";
@@ -17,6 +17,13 @@ import { compileComputedExpr } from "./predicates.js";
17
17
  * @typedef {import("./context.js").Ctx} Ctx
18
18
  */
19
19
 
20
+ // Concrete, compilable one-liners for the state-family hint tails + the catalog.
21
+ export const STATE_EXAMPLE = ":state count = 0";
22
+ export const STORE_EXAMPLE = ":store cart = []";
23
+ export const COMPUTED_EXAMPLE = ":computed total = items.length * 4";
24
+ export const THEME_EXAMPLE = ":theme";
25
+ const STATE_USE = `Use: :state name = value [persist] — e.g. ${STATE_EXAMPLE}`;
26
+
20
27
  /**
21
28
  * @param {string} line
22
29
  * @param {Ctx} ctx
@@ -25,7 +32,14 @@ import { compileComputedExpr } from "./predicates.js";
25
32
  */
26
33
  export function handleState(line, ctx, index) {
27
34
  const match = line.match(/^:state\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+persist)?$/);
28
- if (!match) throw new Error(`Malformed :state in ${at(ctx, index)}: ${line}`);
35
+ if (!match)
36
+ throw wdError(`Malformed :state in ${at(ctx, index)}: ${line}. ${STATE_USE}`, {
37
+ code: "WD201",
38
+ file: ctx.file,
39
+ line: lineOf(ctx, index),
40
+ hint: STATE_USE.slice("Use: ".length),
41
+ example: STATE_EXAMPLE
42
+ });
29
43
  const value = parseStateValue(match[2], at(ctx, index));
30
44
  const key = declareState(match[1], value, ctx);
31
45
  const persistAttr = match[3] ? ` data-wd-persist="${key}"` : "";
@@ -41,14 +55,26 @@ export function handleState(line, ctx, index) {
41
55
  */
42
56
  export function declareState(name, value, ctx) {
43
57
  if (ctx.loopItem)
44
- throw new Error(`State cannot be declared inside a reactive @loop body (${ctx.file})`);
58
+ throw wdError(`State cannot be declared inside a reactive @loop body (${ctx.file})`, {
59
+ code: "WD202",
60
+ file: ctx.file
61
+ });
45
62
  if (ctx.comp.stores.has(name))
46
- throw new Error(
47
- `State "${name}" collides with a :store of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one.`
63
+ throw wdError(
64
+ `State "${name}" collides with a :store of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
65
+ {
66
+ code: "WD203",
67
+ file: ctx.file,
68
+ hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
69
+ example: STORE_EXAMPLE
70
+ }
48
71
  );
49
72
  const key = ctx.sections.length ? `${ctx.sections.at(-1)}:${name}` : name;
50
73
  if (ctx.comp.state.has(key))
51
- throw new Error(`State "${name}" is declared twice in the same scope (${ctx.file})`);
74
+ throw wdError(`State "${name}" is declared twice in the same scope (${ctx.file})`, {
75
+ code: "WD204",
76
+ file: ctx.file
77
+ });
52
78
  ctx.comp.state.set(key, value);
53
79
  ctx.comp.assets.runtime = true;
54
80
  return key;
@@ -63,8 +89,15 @@ export function declareState(name, value, ctx) {
63
89
  export function handleStore(line, ctx, index) {
64
90
  const match = line.match(/^:store\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)(\s+ephemeral)?$/);
65
91
  if (!match)
66
- throw new Error(
67
- `Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral]`
92
+ throw wdError(
93
+ `Malformed :store in ${at(ctx, index)}: ${line}. Use: :store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
94
+ {
95
+ code: "WD205",
96
+ file: ctx.file,
97
+ line: lineOf(ctx, index),
98
+ hint: `:store name = value [ephemeral] — e.g. ${STORE_EXAMPLE}`,
99
+ example: STORE_EXAMPLE
100
+ }
68
101
  );
69
102
  const value = parseStateValue(match[2], at(ctx, index));
70
103
  const name = declareStore(match[1], value, ctx);
@@ -83,10 +116,24 @@ export function handleStore(line, ctx, index) {
83
116
  */
84
117
  export function declareStore(name, value, ctx) {
85
118
  if (ctx.comp.stores.has(name))
86
- throw new Error(`Store "${name}" is declared twice in ${ctx.file}. Use: :store name = value`);
119
+ throw wdError(
120
+ `Store "${name}" is declared twice in ${ctx.file}. Use: :store name = value — e.g. ${STORE_EXAMPLE}`,
121
+ {
122
+ code: "WD206",
123
+ file: ctx.file,
124
+ hint: `:store name = value — e.g. ${STORE_EXAMPLE}`,
125
+ example: STORE_EXAMPLE
126
+ }
127
+ );
87
128
  if (ctx.comp.state.has(name))
88
- throw new Error(
89
- `Store "${name}" collides with a :state of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one.`
129
+ throw wdError(
130
+ `Store "${name}" collides with a :state of the same name in ${ctx.file}. Use: :store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
131
+ {
132
+ code: "WD207",
133
+ file: ctx.file,
134
+ hint: `:store name = value for the global, or rename one — e.g. ${STORE_EXAMPLE}`,
135
+ example: STORE_EXAMPLE
136
+ }
90
137
  );
91
138
  ctx.comp.stores.add(name);
92
139
  ctx.comp.state.set(name, value);
@@ -115,9 +162,13 @@ export function declareErrorState(key, ctx) {
115
162
  export function handleComputed(line, ctx, index) {
116
163
  const match = line.match(/^:computed\s+([A-Za-z_$][\w$]*)\s*=\s*(.+)$/);
117
164
  if (!match)
118
- throw new Error(
119
- `Malformed :computed in ${at(ctx, index)}: ${line}. Use: :computed total = items.length * 4`
120
- );
165
+ throw wdError(`Malformed :computed in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`, {
166
+ code: "WD208",
167
+ file: ctx.file,
168
+ line: lineOf(ctx, index),
169
+ hint: COMPUTED_EXAMPLE,
170
+ example: COMPUTED_EXAMPLE
171
+ });
121
172
  const expr = compileComputedExpr(match[2].trim(), ctx);
122
173
  // Parse the validated expression to the compact AST the runtime walks. A parse
123
174
  // failure means the RHS is syntactically malformed (e.g. `|| <`) — a compile
@@ -127,8 +178,15 @@ export function handleComputed(line, ctx, index) {
127
178
  try {
128
179
  ast = astOf(expr);
129
180
  } catch {
130
- throw new Error(
131
- `Malformed :computed expression in ${at(ctx, index)}: ${line}. Use: :computed total = items.length * 4`
181
+ throw wdError(
182
+ `Malformed :computed expression in ${at(ctx, index)}: ${line}. Use: ${COMPUTED_EXAMPLE}`,
183
+ {
184
+ code: "WD209",
185
+ file: ctx.file,
186
+ line: lineOf(ctx, index),
187
+ hint: COMPUTED_EXAMPLE,
188
+ example: COMPUTED_EXAMPLE
189
+ }
132
190
  );
133
191
  }
134
192
  // Build-time mirror of the runtime: walk the AST (no eval) so `:computed total =
@@ -155,8 +213,15 @@ export function handleComputed(line, ctx, index) {
155
213
  export function handleTheme(line, ctx, index) {
156
214
  const match = line.match(/^:theme(?:\s+([A-Za-z_$][\w$]*))?(?:\s*=\s*(.+))?$/);
157
215
  if (!match) {
158
- throw new Error(
159
- `Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto")`
216
+ throw wdError(
217
+ `Malformed :theme in ${at(ctx, index)}: ${line}. Use: :theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
218
+ {
219
+ code: "WD210",
220
+ file: ctx.file,
221
+ line: lineOf(ctx, index),
222
+ hint: `:theme (or :theme name = "auto") — e.g. ${THEME_EXAMPLE}`,
223
+ example: THEME_EXAMPLE
224
+ }
160
225
  );
161
226
  }
162
227
  const name = match[1] || "theme";
@@ -11,7 +11,7 @@
11
11
  // while keeping the message text and `Use:` hints intact.
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
- import { at, createScope, LOOP_META, nestedCtx } from "./context.js";
14
+ import { at, createScope, LOOP_META, lineOf, nestedCtx, wdError } from "./context.js";
15
15
  import { astOf, serializeExpr } from "./expr-ast.js";
16
16
  import { resolveInclude, scopedSkinFor, stampScope } from "./includes.js";
17
17
  import {
@@ -28,6 +28,13 @@ import { compileWhen, evalPredicate } from "./predicates.js";
28
28
  * @typedef {import("./context.js").Ctx} Ctx
29
29
  */
30
30
 
31
+ // Concrete, compilable structure examples for the hint tails + the catalog.
32
+ export const INCLUDE_EXAMPLE = "@include /header.wd";
33
+ export const CONTAINER_EXAMPLE = "::: card .featured";
34
+ export const IF_EXAMPLE = ":if count > 0";
35
+ export const CAROUSEL_EXAMPLE = ":carousel autoplay=3000";
36
+ const INCLUDE_USE = `Use: @include /partial.wd [with key="value"] — e.g. ${INCLUDE_EXAMPLE}`;
37
+
31
38
  // Container names that emit their own semantic landmark element instead of a
32
39
  // `<div>`, so scaffolds get real `<nav>`/`<main>` landmarks (accessibility: the
33
40
  // skip link targets a genuine main-content region and navigation chrome sits in
@@ -44,7 +51,14 @@ const SEMANTIC_CONTAINER_TAGS = new Set(["nav", "main"]);
44
51
  */
45
52
  export function handleInclude(line, ctx, index) {
46
53
  const match = line.match(/^@include\s+(\S+)(?:\s+with\s+(.+))?$/);
47
- if (!match) throw new Error(`Malformed @include in ${at(ctx, index)}: ${line}`);
54
+ if (!match)
55
+ throw wdError(`Malformed @include in ${at(ctx, index)}: ${line}. ${INCLUDE_USE}`, {
56
+ code: "WD603",
57
+ file: ctx.file,
58
+ line: lineOf(ctx, index),
59
+ hint: INCLUDE_USE.slice("Use: ".length),
60
+ example: INCLUDE_EXAMPLE
61
+ });
48
62
  const target = resolveInclude(
49
63
  match[1],
50
64
  ctx.file,
@@ -94,8 +108,9 @@ function parseIncludeArgs(raw, ctx) {
94
108
  const expr = value.slice(1, -1).trim();
95
109
  const resolved = lookupPath(expr, ctx);
96
110
  if (!resolved.found) {
97
- throw new Error(
98
- `@include argument ${match[1]}={ ${expr} } in ${ctx.file} does not match any value in scope`
111
+ throw wdError(
112
+ `@include argument ${match[1]}={ ${expr} } in ${ctx.file} does not match any value in scope`,
113
+ { code: "WD604", file: ctx.file }
99
114
  );
100
115
  }
101
116
  args[match[1]] = resolved.value;
@@ -152,8 +167,9 @@ export function handleContainer(header, bodyLines, ctx, index) {
152
167
  }
153
168
  const cm = rest.match(/^\.([A-Za-z_][\w-]*)/);
154
169
  if (!cm)
155
- throw new Error(
156
- `Unexpected token "${rest.split(/\s+/)[0]}" in container "::: ${header}" in ${at(ctx, index)}`
170
+ throw wdError(
171
+ `Unexpected token "${rest.split(/\s+/)[0]}" in container "::: ${header}" in ${at(ctx, index)}`,
172
+ { code: "WD605", file: ctx.file, line: lineOf(ctx, index) }
157
173
  );
158
174
  const cls = cm[1];
159
175
  rest = rest.slice(cm[0].length).trim();
@@ -215,10 +231,16 @@ export function handleCarousel(line, bodyLines, ctx, index) {
215
231
  let autoplayAttr = "";
216
232
  if (rest) {
217
233
  const auto = rest.match(/^autoplay=(\d+)$/);
218
- if (!auto)
219
- throw new Error(
220
- `Malformed :carousel in ${at(ctx, index)}: ${line}. Use: :carousel [autoplay=3000] … :endcarousel`
221
- );
234
+ if (!auto) {
235
+ const hint = `:carousel [autoplay=3000] … :endcarousel — e.g. ${CAROUSEL_EXAMPLE}`;
236
+ throw wdError(`Malformed :carousel in ${at(ctx, index)}: ${line}. Use: ${hint}`, {
237
+ code: "WD606",
238
+ file: ctx.file,
239
+ line: lineOf(ctx, index),
240
+ hint,
241
+ example: CAROUSEL_EXAMPLE
242
+ });
243
+ }
222
244
  autoplayAttr = ` data-wd-carousel-autoplay="${auto[1]}"`;
223
245
  }
224
246
  ctx.comp.assets.behaviors.add("carousel");
@@ -277,8 +299,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
277
299
  // Per-row meta vars in :if — only valid inside a loop.
278
300
  if (LOOP_META[head]) {
279
301
  if (!ctx.loopMeta)
280
- throw new Error(
281
- `":if ${match[1]}" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Use it inside a loop body.`
302
+ throw wdError(
303
+ `":if ${match[1]}" uses the loop meta variable "${head}" outside a @loop in ${ctx.file}. Use it inside a loop body.`,
304
+ { code: "WD607", file: ctx.file }
282
305
  );
283
306
  if (ctx.loopItem) {
284
307
  const truthy = ctx.compileBody(truthyLines, truthyCtx).trim();
@@ -304,8 +327,9 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
304
327
 
305
328
  const key = resolveStateKey(head, ctx);
306
329
  if (!key) {
307
- throw new Error(
308
- `:if ${match[1]} in ${ctx.file} does not match a :state or in-scope value. Declare it first.`
330
+ throw wdError(
331
+ `:if ${match[1]} in ${ctx.file} does not match a :state or in-scope value. Declare it first.`,
332
+ { code: "WD608", file: ctx.file }
309
333
  );
310
334
  }
311
335
  ctx.comp.assets.runtime = true;
@@ -323,10 +347,16 @@ export function handleIf(line, truthyLines, falsyLines, ctx, index, falsyStart =
323
347
  // whitelist as `@loop … where` / `.class when` (with `not`), so it folds at
324
348
  // build when static, drives a per-row each-if when it reads the loop item, and
325
349
  // a global if-region (evaluated each render) when it reads state. No raw eval.
326
- if (!condition)
327
- throw new Error(
328
- `Malformed :if in ${at(ctx, index)}: ${line}. Use ":if name" or ":if a <op> b [and|or|not …]".`
329
- );
350
+ if (!condition) {
351
+ const hint = `":if name" or ":if a <op> b [and|or|not …]" — e.g. ${IF_EXAMPLE}`;
352
+ throw wdError(`Malformed :if in ${at(ctx, index)}: ${line}. Use ${hint}`, {
353
+ code: "WD609",
354
+ file: ctx.file,
355
+ line: lineOf(ctx, index),
356
+ hint,
357
+ example: IF_EXAMPLE
358
+ });
359
+ }
330
360
  const compiled = compileWhen(condition, ctx, '":if"');
331
361
  if (compiled.static)
332
362
  return ctx.compileBody(
@@ -380,13 +410,15 @@ function validateDemoHref(href, ctx) {
380
410
  const value = href.trim();
381
411
  // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — rejects control characters in URLs/hrefs.
382
412
  if (value !== href || /[\u0000-\u001F\u007F]/.test(value)) {
383
- throw new Error(
384
- `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`
413
+ throw wdError(
414
+ `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`,
415
+ { code: "WD610", file: ctx.file }
385
416
  );
386
417
  }
387
418
  if (value.startsWith("//")) {
388
- throw new Error(
389
- `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http: or https: explicitly.`
419
+ throw wdError(
420
+ `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Protocol-relative URLs are not allowed; use http: or https: explicitly.`,
421
+ { code: "WD611", file: ctx.file }
390
422
  );
391
423
  }
392
424
  if (
@@ -401,7 +433,8 @@ function validateDemoHref(href, ctx) {
401
433
  if (scheme && ["http", "https", "mailto"].includes(scheme[1].toLowerCase())) {
402
434
  return value;
403
435
  }
404
- throw new Error(
405
- `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`
436
+ throw wdError(
437
+ `Unsafe :try href "${escapeHtml(href)}" in ${ctx.file}. Use a relative URL starting with /, ./, ../, or #, or an http:, https:, or mailto: URL.`,
438
+ { code: "WD610", file: ctx.file }
406
439
  );
407
440
  }
package/src/deploy.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { buildSite } from "./builder.js";
5
+ import { wdError } from "./compiler/context.js";
5
6
  import { BASE_SECURITY_HEADERS, REACTIVE_CSP } from "./headers.js";
6
7
 
7
8
  /** The platforms `darkmown deploy` knows how to drive. */
@@ -25,7 +26,9 @@ export const DEPLOY_TARGETS = ["vercel", "cloudflare"];
25
26
  */
26
27
  export async function deploy({ cwd, target, prod = false, log, run = spawnProcess, projectName }) {
27
28
  if (!DEPLOY_TARGETS.includes(target)) {
28
- throw new Error(`Unknown deploy target "${target}". Use one of: ${DEPLOY_TARGETS.join(", ")}.`);
29
+ throw wdError(`Unknown deploy target "${target}". Use one of: ${DEPLOY_TARGETS.join(", ")}.`, {
30
+ code: "WD904"
31
+ });
29
32
  }
30
33
 
31
34
  ensurePlatformConfig(cwd, target);
@@ -46,11 +49,12 @@ export async function deploy({ cwd, target, prod = false, log, run = spawnProces
46
49
  output
47
50
  )
48
51
  ) {
49
- throw new Error(
50
- `The ${target} CLI is not signed in. Run \`${plan.loginHint}\` (try \`! ${plan.loginHint}\` in this session), then re-run \`darkmown deploy ${target}\`.`
52
+ throw wdError(
53
+ `The ${target} CLI is not signed in. Run \`${plan.loginHint}\` (try \`! ${plan.loginHint}\` in this session), then re-run \`darkmown deploy ${target}\`.`,
54
+ { code: "WD905" }
51
55
  );
52
56
  }
53
- throw new Error(`${target} deploy failed (exit ${result.code}).`);
57
+ throw wdError(`${target} deploy failed (exit ${result.code}).`, { code: "WD906" });
54
58
  }
55
59
 
56
60
  const url = plan.parseUrl(output);