@stacksjs/stx 0.2.342 → 0.2.343

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 (167) hide show
  1. package/dist/caching.js +1 -0
  2. package/dist/{chunk-48x5bsqv.js → chunk-0dpgh7a6.js} +1 -1
  3. package/dist/{chunk-evtrctj9.js → chunk-0fp16c1x.js} +1 -1
  4. package/dist/{chunk-p5d8sb50.js → chunk-0s3ry262.js} +1 -1
  5. package/dist/{chunk-tmj45xq5.js → chunk-12qteqk8.js} +1 -1
  6. package/dist/chunk-160g7ynw.js +2 -0
  7. package/dist/chunk-16hr5y08.js +2 -0
  8. package/dist/{chunk-anf2y2h5.js → chunk-1bfvdjpy.js} +1 -1
  9. package/dist/{chunk-y956wapy.js → chunk-1h1c2048.js} +1 -1
  10. package/dist/{chunk-bt8w146r.js → chunk-1qtd4ex2.js} +2 -2
  11. package/dist/{chunk-2swnc8kg.js → chunk-1zav2sf1.js} +1 -1
  12. package/dist/{chunk-gbkq5qnw.js → chunk-2853ye06.js} +2 -2
  13. package/dist/{chunk-4vcyf1kh.js → chunk-36j9qset.js} +2 -2
  14. package/dist/{chunk-74r1zw2h.js → chunk-3d3x7dwy.js} +1 -1
  15. package/dist/chunk-3h97y3n3.js +2 -0
  16. package/dist/chunk-3j915h2d.js +2 -0
  17. package/dist/{chunk-1gwbw81h.js → chunk-45ydb29q.js} +1 -1
  18. package/dist/{chunk-ts3wfjh9.js → chunk-4fafxr8d.js} +1 -1
  19. package/dist/chunk-4hkg6kx4.js +2 -0
  20. package/dist/{chunk-8tsr51hj.js → chunk-4pg6wbt6.js} +1 -1
  21. package/dist/{chunk-saqbtgqr.js → chunk-4vd0rqfs.js} +1 -1
  22. package/dist/{chunk-wv8h3d68.js → chunk-53sjbp3z.js} +1 -1
  23. package/dist/chunk-57494khk.js +2 -0
  24. package/dist/{chunk-4wpxbkbe.js → chunk-57dxdvjh.js} +1 -1
  25. package/dist/{chunk-k3w3gwm3.js → chunk-5azeswj2.js} +1 -1
  26. package/dist/{chunk-k5b9471y.js → chunk-5hq25yky.js} +1 -1
  27. package/dist/chunk-5xavmr4t.js +2 -0
  28. package/dist/{chunk-kznb3hdj.js → chunk-5zemhhwe.js} +1 -1
  29. package/dist/{chunk-ghd80w8a.js → chunk-62vng8g8.js} +2 -2
  30. package/dist/{chunk-reskzmsb.js → chunk-649dfqqz.js} +1 -1
  31. package/dist/{chunk-6nxq8rcw.js → chunk-6g8rvnhc.js} +1 -1
  32. package/dist/chunk-6rsfkms1.js +2 -0
  33. package/dist/{chunk-xe93ae3r.js → chunk-6xs80tka.js} +3 -3
  34. package/dist/{chunk-s4mmmwj4.js → chunk-7mc1k1tq.js} +1 -1
  35. package/dist/{chunk-25b8z7xa.js → chunk-7r79j1hs.js} +1 -1
  36. package/dist/{chunk-drs93sry.js → chunk-7yfk9393.js} +1 -1
  37. package/dist/{chunk-r46cg4qa.js → chunk-7zxv2631.js} +1 -1
  38. package/dist/{chunk-d389vyyz.js → chunk-8ah5mhst.js} +2 -2
  39. package/dist/{chunk-zv4gb3wb.js → chunk-905j49fe.js} +1 -1
  40. package/dist/chunk-9czj483k.js +2 -0
  41. package/dist/chunk-9kv0qavn.js +2 -0
  42. package/dist/{chunk-m39hb2a2.js → chunk-a0ajg9z5.js} +1 -1
  43. package/dist/{chunk-mznr9nyp.js → chunk-ag8gdk51.js} +1 -1
  44. package/dist/{chunk-gwc54hjc.js → chunk-amfq5rnz.js} +2 -2
  45. package/dist/{chunk-rh7mwwb4.js → chunk-aqxwwq6w.js} +2 -2
  46. package/dist/{chunk-hk8j3eps.js → chunk-ay336sqb.js} +1 -1
  47. package/dist/chunk-bcz8yp2g.js +2 -0
  48. package/dist/{chunk-rr49zrz6.js → chunk-brjfzbq9.js} +1 -1
  49. package/dist/chunk-bxefsc7k.js +2 -0
  50. package/dist/chunk-c4kv3r8t.js +2 -0
  51. package/dist/{chunk-yzv702kk.js → chunk-c7qdnc5d.js} +1 -1
  52. package/dist/{chunk-ys56ecvx.js → chunk-daxr6p6q.js} +1 -1
  53. package/dist/{chunk-yhvrq2qd.js → chunk-dj5mqnp1.js} +1 -1
  54. package/dist/{chunk-da0tfpa2.js → chunk-e53pwshq.js} +1 -1
  55. package/dist/{chunk-k9ep24bd.js → chunk-e6rxvz3p.js} +1 -1
  56. package/dist/{chunk-x7dv5rq7.js → chunk-ec18ftcg.js} +1 -1
  57. package/dist/chunk-eh3meyd3.js +64 -0
  58. package/dist/{chunk-yq7fc8sa.js → chunk-em8cr7wy.js} +1 -1
  59. package/dist/{chunk-thrv2nbz.js → chunk-ey1tsk22.js} +1 -1
  60. package/dist/{chunk-e0kzyf1g.js → chunk-fbmjw0bh.js} +1 -1
  61. package/dist/chunk-fytemc96.js +2 -0
  62. package/dist/{chunk-zv4nf0x2.js → chunk-g0dbtyk7.js} +1 -1
  63. package/dist/chunk-g7gc0ten.js +2 -0
  64. package/dist/chunk-gqsxxd81.js +2 -0
  65. package/dist/{chunk-sytwy0mg.js → chunk-grnmr3y1.js} +1 -1
  66. package/dist/{chunk-4ecv1e3w.js → chunk-gvfgrve8.js} +1 -1
  67. package/dist/chunk-gx9z0wmm.js +2 -0
  68. package/dist/{chunk-3b0mgxbk.js → chunk-h6y99nak.js} +1 -1
  69. package/dist/chunk-hq3phk1a.js +3 -0
  70. package/dist/{chunk-01ggk8k8.js → chunk-hrdd26r4.js} +1 -1
  71. package/dist/{chunk-567g1ttv.js → chunk-hzbty7hz.js} +1 -1
  72. package/dist/{chunk-6nvm939g.js → chunk-jagf7h9y.js} +1 -1
  73. package/dist/{chunk-xec226zb.js → chunk-jh6f1a9y.js} +1 -1
  74. package/dist/chunk-jnxzzzm4.js +2 -0
  75. package/dist/chunk-jy6jwxhs.js +2 -0
  76. package/dist/{chunk-r1rfwm01.js → chunk-k3dtvay1.js} +1 -1
  77. package/dist/chunk-k8skvm0d.js +2 -0
  78. package/dist/{chunk-1xwfws06.js → chunk-kbah2k8e.js} +2 -2
  79. package/dist/{chunk-beextc84.js → chunk-kgbz86sa.js} +2 -2
  80. package/dist/chunk-khqn40gv.js +2 -0
  81. package/dist/{chunk-fx29n4vc.js → chunk-m0xaw0qw.js} +1 -1
  82. package/dist/chunk-mhbzrh5y.js +2 -0
  83. package/dist/{chunk-mbstfjnj.js → chunk-mwvxhv0r.js} +1 -1
  84. package/dist/{chunk-cqw3c1xg.js → chunk-p3awq80d.js} +1 -1
  85. package/dist/chunk-p8anntjb.js +2 -0
  86. package/dist/chunk-pwe51myj.js +2 -0
  87. package/dist/{chunk-gkq323fg.js → chunk-pwhf5kte.js} +1 -1
  88. package/dist/{chunk-25f0s8t1.js → chunk-q6825wwz.js} +1 -1
  89. package/dist/{chunk-f2qbjr3z.js → chunk-q9ckfwhh.js} +1 -1
  90. package/dist/{chunk-ytx17bg2.js → chunk-rns7wjy9.js} +1 -1
  91. package/dist/chunk-rqqdvnp5.js +2 -0
  92. package/dist/chunk-rzg9pars.js +7 -0
  93. package/dist/{chunk-yr7camcd.js → chunk-s60eccqk.js} +1 -1
  94. package/dist/{chunk-ntm1761b.js → chunk-s7qbba8r.js} +10 -10
  95. package/dist/{chunk-hrmxxs0w.js → chunk-safmgrq5.js} +1 -1
  96. package/dist/{chunk-ebd3z4j9.js → chunk-stqm4brp.js} +1 -1
  97. package/dist/{chunk-1t3j8a0q.js → chunk-t00ga194.js} +1 -1
  98. package/dist/{chunk-fm5211ec.js → chunk-tqw1yean.js} +1 -1
  99. package/dist/{chunk-hnffkdzc.js → chunk-tz68qv47.js} +1 -1
  100. package/dist/{chunk-1spzqjve.js → chunk-vaggjvsm.js} +1 -1
  101. package/dist/{chunk-q4cemvkw.js → chunk-vj3nx26v.js} +1 -1
  102. package/dist/chunk-w1mbrq1g.js +2 -0
  103. package/dist/chunk-w84qe2m6.js +2 -0
  104. package/dist/{chunk-c33y19bn.js → chunk-wdaf98ya.js} +1 -1
  105. package/dist/chunk-wjm7hv5e.js +2 -0
  106. package/dist/{chunk-byjy9far.js → chunk-wnmegbw0.js} +1 -1
  107. package/dist/{chunk-0sq5mdrh.js → chunk-wyhb3nb0.js} +1 -1
  108. package/dist/{chunk-j82hfkcf.js → chunk-xgw269cf.js} +234 -236
  109. package/dist/{chunk-jcbj3xp4.js → chunk-y5sxa2sq.js} +1 -1
  110. package/dist/{chunk-mkgbrbr9.js → chunk-yefnrr5z.js} +1 -1
  111. package/dist/chunk-yr7s02f7.js +2 -0
  112. package/dist/{chunk-p52cm7ch.js → chunk-z9hb80mk.js} +1 -1
  113. package/dist/{chunk-c73pyeak.js → chunk-zcytk5w9.js} +1 -1
  114. package/dist/chunk-zq3tm2c2.js +2 -0
  115. package/dist/{chunk-5g9vbhbt.js → chunk-zr1x54bt.js} +1 -1
  116. package/dist/cli.js +14 -14
  117. package/dist/craft-entry.js +1 -1
  118. package/dist/craft.js +1 -1
  119. package/dist/custom-directives.js +21 -8
  120. package/dist/database.js +1 -1
  121. package/dist/directive-arguments.d.ts +107 -0
  122. package/dist/directive-arguments.js +171 -0
  123. package/dist/head.d.ts +7 -30
  124. package/dist/head.js +15 -5
  125. package/dist/meta-arguments.d.ts +52 -0
  126. package/dist/meta-arguments.js +55 -0
  127. package/dist/process.js +1 -1
  128. package/dist/pwa.js +2 -2
  129. package/dist/seo.d.ts +39 -17
  130. package/dist/seo.js +221 -255
  131. package/dist/ssg.js +2 -2
  132. package/dist/types/directive-types.d.ts +12 -0
  133. package/dist/visual-testing.js +1 -1
  134. package/package.json +5 -5
  135. package/dist/chunk-025z2btp.js +0 -2
  136. package/dist/chunk-19fp0z9s.js +0 -2
  137. package/dist/chunk-23sv24cm.js +0 -2
  138. package/dist/chunk-2p68bkza.js +0 -2
  139. package/dist/chunk-5grrqaqs.js +0 -2
  140. package/dist/chunk-6jx4r94x.js +0 -2
  141. package/dist/chunk-6p6b01kp.js +0 -4
  142. package/dist/chunk-a1rt1fms.js +0 -2
  143. package/dist/chunk-ahpmjg3f.js +0 -64
  144. package/dist/chunk-akp89n3w.js +0 -2
  145. package/dist/chunk-bqhrryqs.js +0 -2
  146. package/dist/chunk-czvzaa42.js +0 -3
  147. package/dist/chunk-de2cfcf5.js +0 -2
  148. package/dist/chunk-exhbc57n.js +0 -2
  149. package/dist/chunk-fhhp6pmx.js +0 -2
  150. package/dist/chunk-fqsrft0a.js +0 -2
  151. package/dist/chunk-gqzccw30.js +0 -2
  152. package/dist/chunk-h47e1c03.js +0 -2
  153. package/dist/chunk-kbemcvp1.js +0 -2
  154. package/dist/chunk-ks4j8fjz.js +0 -2
  155. package/dist/chunk-pbct6nxk.js +0 -2
  156. package/dist/chunk-peerbcwj.js +0 -2
  157. package/dist/chunk-pt1w6vq2.js +0 -2
  158. package/dist/chunk-rgda0gwf.js +0 -2
  159. package/dist/chunk-rwa10jm4.js +0 -2
  160. package/dist/chunk-snsh9ye4.js +0 -2
  161. package/dist/chunk-td39mtts.js +0 -2
  162. package/dist/chunk-trncnwem.js +0 -2
  163. package/dist/chunk-vk2j3vvs.js +0 -2
  164. package/dist/chunk-vph6zpeq.js +0 -2
  165. package/dist/chunk-wa3ks5zv.js +0 -2
  166. package/dist/chunk-xer5kn26.js +0 -2
  167. package/dist/chunk-zx2krbqf.js +0 -2
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Argument handling for directives whose arguments are expressions.
3
+ *
4
+ * Written for the SEO directives (`@seo`, `@meta`, `@metaTag`,
5
+ * `@structuredData`), and kept free of anything SEO-specific so a custom
6
+ * directive can use it too (`CustomDirective.rawParams`).
7
+ *
8
+ * Every one of these used to fail the same way: an argument that was not the
9
+ * one shape its regex expected was either left in the page as literal text or
10
+ * replaced with nothing, and no one was told. `@seo(seo)` never matched a
11
+ * pattern that insisted on `{`; `@meta('author', post.author)` sent the words
12
+ * "post.author" as the author; `@structuredData(prodcut)` emitted a JSON-LD
13
+ * block holding nothing but `@context`. Each page looked fine and was wrong
14
+ * where it counts, in a link preview or a search result.
15
+ *
16
+ * So the directives share one way to find their call (balanced and quote-aware,
17
+ * so `'Smile :)'` does not end the call early), one way to tell a string literal
18
+ * from an expression, one evaluator that can say WHY a value is missing, and one
19
+ * way to report a failure: an inline comment where the tag would have been, plus
20
+ * a console warning naming the file.
21
+ */
22
+ import type { ErrorCode } from './errors/codes';
23
+ export interface DirectiveCall {
24
+ /** Index of the `@`. */
25
+ start: number;
26
+ /** Index one past the closing paren, or -1 when the call is never closed. */
27
+ end: number;
28
+ /** The text between the parens, as written. */
29
+ args: string;
30
+ /** The whole call, `@name(...)`, or its first 80 characters when unclosed. */
31
+ call: string;
32
+ }
33
+ /**
34
+ * Every `@name(...)` call in `template` matched by `pattern`, in order.
35
+ *
36
+ * `pattern` must be global and end at the opening paren. The closing paren is
37
+ * found by balance, skipping parens inside strings, so an argument may hold any
38
+ * expression: `@seo(build({ title: 'Smile :)' }))` is one call.
39
+ */
40
+ export declare function findDirectiveCalls(template: string, pattern: RegExp): DirectiveCall[];
41
+ /**
42
+ * Replace every call {@link findDirectiveCalls} finds.
43
+ *
44
+ * One left-to-right pass over the original text: a replacement is never
45
+ * rescanned, so markup that happens to contain the directive's name cannot
46
+ * loop. A call with no closing paren keeps its text, preceded by whatever
47
+ * `onUnterminated` returns, so the author sees both the report and the call.
48
+ */
49
+ export declare function replaceDirectiveCalls(template: string, pattern: RegExp, replace: (args: string, call: string) => string, onUnterminated: (call: string) => string): string;
50
+ /**
51
+ * Split an argument list at its top-level commas.
52
+ *
53
+ * Commas inside strings, template literals, brackets, braces and parens belong
54
+ * to the argument they sit in: `'a, b', fn(x, y)` is two arguments, not four.
55
+ * Arguments come back trimmed and exactly as written, quotes included, so the
56
+ * caller can still tell `'title'` (the word) from `title` (a variable).
57
+ */
58
+ export declare function splitTopLevelArgs(args: string): string[];
59
+ /**
60
+ * The value of `raw` when it is exactly one string literal, otherwise `null`.
61
+ *
62
+ * `'a' + b`, `'a' || 'b'` and a template literal with `${}` in it are
63
+ * expressions and return `null`. A literal is read here rather than handed to
64
+ * the evaluator because the evaluator's safety screen reads words, and a
65
+ * description is allowed to say "eval".
66
+ */
67
+ export declare function parseStringLiteral(raw: string): string | null;
68
+ export type EvaluatedArgument = {
69
+ ok: true;
70
+ value: unknown;
71
+ } | {
72
+ ok: false;
73
+ message: string;
74
+ code: ErrorCode;
75
+ };
76
+ /**
77
+ * Evaluate a directive argument against the template's context.
78
+ *
79
+ * The shared evaluator turns every failure into `undefined`, which is right for
80
+ * `{{ }}` and wrong here: a directive whose whole job is to produce a tag has
81
+ * to be able to say why it produced none. So the three ways an expression
82
+ * fails are told apart:
83
+ *
84
+ * - it is not allowed (the safety screen) or not JavaScript (a syntax error);
85
+ * - it names a variable the template does not have, which is almost always a
86
+ * value declared in a plain (client) `<script>`, or a typo;
87
+ * - it is an object or array literal that came back `undefined`, which only
88
+ * happens when something inside it threw.
89
+ *
90
+ * A defined variable that simply holds `null` or `undefined` is not a failure:
91
+ * `@seo(page.seo)` on a page without one is allowed to render nothing.
92
+ */
93
+ export declare function evaluateDirectiveArgument(expression: string, context: Record<string, unknown>): EvaluatedArgument;
94
+ /** How a value reads in an error message. */
95
+ export declare function describeValue(value: unknown): string;
96
+ /** A plain object: not null, not an array. */
97
+ export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
98
+ /** The call as it reads in a message: one line, and not the whole object. */
99
+ export declare function shortCall(call: string): string;
100
+ /**
101
+ * Report a directive that produced nothing: a console warning naming the file,
102
+ * and an inline comment where the tag would have been, for whoever reads the
103
+ * built HTML (which is how most of these were found).
104
+ */
105
+ export declare function reportDirectiveFailure(label: string, call: string, message: string, filePath: string, code?: ErrorCode): string;
106
+ /** Warn without replacing anything: the directive rendered, but not all of it. */
107
+ export declare function warnDirective(call: string, message: string, filePath: string): void;
@@ -0,0 +1,171 @@
1
+ import path from "node:path";
2
+ import { ErrorCodes, inlineError } from "./error-handling.js";
3
+ import { findMatchingDelimiter } from "./parser/tokenizer.js";
4
+ import { createSafeContext, createSafeFunction, freeIdentifiers, getExpressionSafetyRule } from "./safe-evaluator.js";
5
+ export function findDirectiveCalls(template, pattern) {
6
+ const calls = [];
7
+ pattern.lastIndex = 0;
8
+ let match;
9
+ while ((match = pattern.exec(template)) !== null) {
10
+ const start = match.index, openParen = start + match[0].length - 1, closeParen = findMatchingDelimiter(template, "(", ")", openParen);
11
+ if (closeParen === -1) {
12
+ calls.push({ start, end: -1, args: "", call: template.slice(start, Math.min(template.length, start + 80)) });
13
+ pattern.lastIndex = openParen + 1;
14
+ continue;
15
+ }
16
+ calls.push({
17
+ start,
18
+ end: closeParen + 1,
19
+ args: template.slice(openParen + 1, closeParen),
20
+ call: template.slice(start, closeParen + 1)
21
+ });
22
+ pattern.lastIndex = closeParen + 1;
23
+ }
24
+ return calls;
25
+ }
26
+ export function replaceDirectiveCalls(template, pattern, replace, onUnterminated) {
27
+ let output = "", cursor = 0;
28
+ for (const found of findDirectiveCalls(template, pattern)) {
29
+ output += template.slice(cursor, found.start);
30
+ if (found.end === -1) {
31
+ output += onUnterminated(found.call);
32
+ cursor = found.start;
33
+ continue;
34
+ }
35
+ output += replace(found.args, found.call);
36
+ cursor = found.end;
37
+ }
38
+ return output + template.slice(cursor);
39
+ }
40
+ export function splitTopLevelArgs(args) {
41
+ const parts = [];
42
+ let depth = 0, quote = null, current = "";
43
+ for (let i = 0;i < args.length; i++) {
44
+ const char = args[i];
45
+ if (quote) {
46
+ current += char;
47
+ if (char === "\\" && i + 1 < args.length) {
48
+ current += args[++i];
49
+ continue;
50
+ }
51
+ if (char === quote)
52
+ quote = null;
53
+ continue;
54
+ }
55
+ if (char === "'" || char === '"' || char === "`")
56
+ quote = char;
57
+ else if (char === "(" || char === "[" || char === "{")
58
+ depth++;
59
+ else if (char === ")" || char === "]" || char === "}")
60
+ depth--;
61
+ else if (char === "," && depth === 0) {
62
+ parts.push(current.trim());
63
+ current = "";
64
+ continue;
65
+ }
66
+ current += char;
67
+ }
68
+ if (current.trim())
69
+ parts.push(current.trim());
70
+ return parts;
71
+ }
72
+ export function parseStringLiteral(raw) {
73
+ const text = raw.trim(), quote = text[0];
74
+ if (quote !== "'" && quote !== '"' && quote !== "`" || text.length < 2 || text.at(-1) !== quote)
75
+ return null;
76
+ let value = "";
77
+ for (let i = 1;i < text.length - 1; i++) {
78
+ const char = text[i];
79
+ if (char === "\\" && i + 1 < text.length - 1) {
80
+ const next = text[++i];
81
+ value += next === "n" ? `
82
+ ` : next === "t" ? "\t" : next;
83
+ continue;
84
+ }
85
+ if (char === quote)
86
+ return null;
87
+ if (quote === "`" && char === "$" && text[i + 1] === "{")
88
+ return null;
89
+ value += char;
90
+ }
91
+ return value;
92
+ }
93
+ const NOT_VARIABLES = new Set([
94
+ "true",
95
+ "false",
96
+ "null",
97
+ "undefined",
98
+ "typeof",
99
+ "void",
100
+ "new",
101
+ "in",
102
+ "of",
103
+ "instanceof",
104
+ "this",
105
+ "delete",
106
+ "await",
107
+ "async",
108
+ "function",
109
+ "return",
110
+ "NaN",
111
+ "Infinity"
112
+ ]);
113
+ export function evaluateDirectiveArgument(expression, context) {
114
+ const source = expression.trim();
115
+ if (!source)
116
+ return { ok: !1, message: "the argument is empty", code: ErrorCodes.INVALID_DIRECTIVE_SYNTAX };
117
+ const rule = getExpressionSafetyRule(source);
118
+ if (rule)
119
+ return { ok: !1, message: `the expression is not allowed (${rule})`, code: ErrorCodes.UNSAFE_EXPRESSION };
120
+ const safeContext = createSafeContext(context);
121
+ let value;
122
+ try {
123
+ value = createSafeFunction(source, Object.keys(safeContext))(...Object.values(safeContext));
124
+ } catch (error) {
125
+ return { ok: !1, message: `the expression could not be evaluated: ${error instanceof Error ? error.message : String(error)}`, code: ErrorCodes.EVALUATION_ERROR };
126
+ }
127
+ if (value !== void 0 && value !== null)
128
+ return { ok: !0, value };
129
+ const missing = [...new Set(freeIdentifiers(source))].filter((name) => !NOT_VARIABLES.has(name) && !(name in safeContext) && !(name in globalThis));
130
+ if (missing.length > 0)
131
+ return {
132
+ ok: !1,
133
+ message: `${missing.map((name) => `"${name}"`).join(", ")} ${missing.length === 1 ? "is" : "are"} not defined here. Values a directive reads must come from <script server> or the render context; a plain <script> runs in the browser. To pass literal text, quote it.`,
134
+ code: ErrorCodes.UNDEFINED_VARIABLE
135
+ };
136
+ if (value === void 0 && (source.startsWith("{") || source.startsWith("[")))
137
+ return { ok: !1, message: "the expression threw while it was evaluated", code: ErrorCodes.EVALUATION_ERROR };
138
+ return { ok: !0, value };
139
+ }
140
+ export function describeValue(value) {
141
+ if (value === null)
142
+ return "null";
143
+ if (Array.isArray(value))
144
+ return "an array";
145
+ if (typeof value === "object")
146
+ return "an object";
147
+ if (typeof value === "string")
148
+ return `a string ("${value.length > 40 ? `${value.slice(0, 40)}...` : value}")`;
149
+ return `${/^[aeiou]/.test(typeof value) ? "an" : "a"} ${typeof value}`;
150
+ }
151
+ export function isPlainRecord(value) {
152
+ return typeof value === "object" && value !== null && !Array.isArray(value);
153
+ }
154
+ function displayPath(filePath) {
155
+ if (!filePath)
156
+ return "";
157
+ return path.isAbsolute(filePath) ? path.relative(process.cwd(), filePath) || filePath : filePath;
158
+ }
159
+ export function shortCall(call) {
160
+ const flat = call.replace(/\s+/g, " ").trim();
161
+ return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
162
+ }
163
+ export function reportDirectiveFailure(label, call, message, filePath, code = ErrorCodes.EVALUATION_ERROR) {
164
+ const where = filePath ? ` in ${displayPath(filePath)}` : "";
165
+ console.warn(`[stx] ${shortCall(call)}${where}: ${message}`);
166
+ return inlineError(label, `${shortCall(call)}: ${message}`, code);
167
+ }
168
+ export function warnDirective(call, message, filePath) {
169
+ const where = filePath ? ` in ${displayPath(filePath)}` : "";
170
+ console.warn(`[stx] ${shortCall(call)}${where}: ${message}`);
171
+ }
package/dist/head.d.ts CHANGED
@@ -1,32 +1,3 @@
1
- /**
2
- * STX Head Management
3
- *
4
- * Manage document head (title, meta, links, scripts) with a simple API.
5
- *
6
- * @example
7
- * ```typescript
8
- * import { useHead, useSeoMeta, definePageMeta } from 'stx'
9
- *
10
- * // Full control
11
- * useHead({
12
- * title: 'My Page',
13
- * meta: [{ name: 'description', content: '...' }]
14
- * })
15
- *
16
- * // SEO shorthand
17
- * useSeoMeta({
18
- * title: 'My Page',
19
- * description: 'Page description',
20
- * ogImage: '/og.png'
21
- * })
22
- *
23
- * // Page configuration
24
- * definePageMeta({
25
- * title: 'My Page',
26
- * layout: 'default'
27
- * })
28
- * ```
29
- */
30
1
  export interface MetaTag {
31
2
  name?: string;
32
3
  property?: string;
@@ -284,11 +255,17 @@ export declare function processTitleDirective(content: string, context: Record<s
284
255
  /**
285
256
  * Process @meta directive.
286
257
  *
258
+ * Stages each tag on the render's head. Reached only when the render has no
259
+ * custom directives, since the registered `metaDirective` (seo.ts) takes every
260
+ * `@meta` first; both resolve their arguments with `resolveMetaArguments`, so
261
+ * a quoted value is text and anything else is an expression.
262
+ *
287
263
  * @example
288
264
  * ```html
289
265
  * @meta('description', 'Page description')
290
266
  * @meta('og:image', ogImageUrl)
267
+ * @meta('author', post.author)
291
268
  * ```
292
269
  */
293
- export declare function processMetaDirective(content: string, context: Record<string, unknown>): string;
270
+ export declare function processMetaDirective(content: string, context: Record<string, unknown>, filePath?: string): string;
294
271
  export { usePageTitle as useTitle };
package/dist/head.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { replaceDirectiveCalls, reportDirectiveFailure, splitTopLevelArgs } from "./directive-arguments.js";
2
+ import { ErrorCodes } from "./error-handling.js";
3
+ import { resolveMetaArguments } from "./meta-arguments.js";
1
4
  let currentHead = {};
2
5
  const headStack = [];
3
6
  let currentPageMeta = {};
@@ -237,12 +240,19 @@ export function processTitleDirective(content, context) {
237
240
  return "";
238
241
  });
239
242
  }
240
- export function processMetaDirective(content, context) {
241
- return content.replace(/@meta\s*\(\s*(['"`])(.+?)\1\s*,\s*(?:(['"`])(.+?)\3|([^)]+))\s*\)/g, (_, _q1, name, q2, quotedValue, unquotedValue) => {
242
- const value = q2 ? quotedValue : unquotedValue, resolvedValue = q2 ? value : context[(value || "").trim()] ?? value, meta = name.includes(":") ? { property: name, content: resolvedValue } : { name, content: resolvedValue }, existing = context.__stx_runtime_head ?? {};
243
- context.__stx_runtime_head = mergeHeadConfigs(existing, { meta: [meta] });
243
+ export function processMetaDirective(content, context, filePath = "") {
244
+ if (!content.includes("@meta"))
245
+ return content;
246
+ return replaceDirectiveCalls(content, /(?<![\w$@])@meta\s*\(/g, (args, call) => {
247
+ const resolved = resolveMetaArguments(splitTopLevelArgs(args), context);
248
+ if (resolved.kind === "error")
249
+ return reportDirectiveFailure("Meta", call, resolved.message, filePath, resolved.code);
250
+ if (resolved.kind === "tag") {
251
+ const existing = context.__stx_runtime_head ?? {};
252
+ context.__stx_runtime_head = mergeHeadConfigs(existing, { meta: [resolved.tag] });
253
+ }
244
254
  return "";
245
- });
255
+ }, (call) => reportDirectiveFailure("Meta", call, "the call has no closing parenthesis", filePath, ErrorCodes.INVALID_DIRECTIVE_SYNTAX));
246
256
  }
247
257
 
248
258
  export {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolving `@meta(...)` arguments to a tag.
3
+ *
4
+ * `@meta` has three entry points -- the registered custom directive that stages
5
+ * the tag on the head (seo.ts `metaDirective`), the head pass for renders
6
+ * without custom directives (head.ts `processMetaDirective`), and the inline
7
+ * pass that partials use (seo.ts `processMetaDirectives`) -- and they used to
8
+ * read their arguments three different ways. A quoted value could be looked up
9
+ * in the context as if it were a variable name, and an unquoted one that was
10
+ * not a bare variable (`post.author`) was sent as literal text. One resolver
11
+ * now serves all three. It lives apart from both seo.ts and head.ts because
12
+ * seo.ts already imports head.ts.
13
+ */
14
+ import type { ErrorCode } from './errors/codes';
15
+ /**
16
+ * Namespaces whose tags are addressed by `property`, not `name`.
17
+ *
18
+ * Open Graph and its relatives are RDFa vocabularies, so the attribute is
19
+ * `property`; a `<meta name="og:title">` is ignored by every scraper that
20
+ * implements the spec. Twitter cards are the exception people expect to be a
21
+ * property and are not: the card spec says `name`.
22
+ */
23
+ export declare const PROPERTY_NAMESPACES: string[];
24
+ export type MetaTagAttrs = {
25
+ name: string;
26
+ content: string;
27
+ } | {
28
+ property: string;
29
+ content: string;
30
+ };
31
+ export type ResolvedMeta = {
32
+ kind: 'tag';
33
+ tag: MetaTagAttrs;
34
+ } | {
35
+ kind: 'empty';
36
+ } | {
37
+ kind: 'error';
38
+ message: string;
39
+ code: ErrorCode;
40
+ };
41
+ /**
42
+ * Resolve `@meta(...)` arguments, given as written (quotes kept), to a tag.
43
+ *
44
+ * - `@meta('author', 'Jane')`: literal text. A quoted value is never looked up
45
+ * in the context, so `@meta('description', 'title')` means the word.
46
+ * - `@meta('author', author)`, `@meta('og:title', post.title)`,
47
+ * `@meta('keywords', tags)`: the second argument is evaluated. An array joins
48
+ * with ", "; `null`, `undefined`, `false` and `''` render nothing.
49
+ * - `@meta('og:title')`: the one-argument form reads the context by the part
50
+ * after the colon (`title`), then `openGraph.title`.
51
+ */
52
+ export declare function resolveMetaArguments(args: string[], context: Record<string, unknown>): ResolvedMeta;
@@ -0,0 +1,55 @@
1
+ import { ErrorCodes } from "./error-handling.js";
2
+ import { describeValue, evaluateDirectiveArgument, isPlainRecord, parseStringLiteral } from "./directive-arguments.js";
3
+ export const PROPERTY_NAMESPACES = ["og:", "article:", "book:", "profile:", "fb:", "music:", "video:"];
4
+ const BARE_META_NAME = /^[a-z][\w-]*(?::[\w:-]+)?$/i;
5
+ export function resolveMetaArguments(args, context) {
6
+ if (args.length < 1)
7
+ return { kind: "error", message: "meta directive requires at least the meta name", code: ErrorCodes.INVALID_DIRECTIVE_SYNTAX };
8
+ if (args.length > 2)
9
+ return { kind: "error", message: `@meta takes a name and a value, got ${args.length} arguments`, code: ErrorCodes.INVALID_DIRECTIVE_SYNTAX };
10
+ const name = resolveMetaName(args[0], context);
11
+ if (typeof name !== "string")
12
+ return name;
13
+ let content;
14
+ if (args.length === 2) {
15
+ const value = resolveMetaValue(args[1], context);
16
+ if (typeof value !== "string")
17
+ return value;
18
+ content = value;
19
+ } else {
20
+ const segment = name.includes(":") ? name.slice(name.indexOf(":") + 1) : name, openGraph = context.openGraph, found = context[segment] ?? (isPlainRecord(openGraph) ? openGraph[segment] : void 0);
21
+ content = found === void 0 || found === null || typeof found === "object" ? "" : String(found);
22
+ }
23
+ if (!content)
24
+ return { kind: "empty" };
25
+ return { kind: "tag", tag: PROPERTY_NAMESPACES.some((prefix) => name.startsWith(prefix)) ? { property: name, content } : { name, content } };
26
+ }
27
+ function resolveMetaName(raw, context) {
28
+ const literal = parseStringLiteral(raw);
29
+ if (literal !== null)
30
+ return literal.trim() ? literal : { kind: "error", message: "the meta name is empty", code: ErrorCodes.INVALID_DIRECTIVE_SYNTAX };
31
+ if (BARE_META_NAME.test(raw.trim()))
32
+ return raw.trim();
33
+ const result = evaluateDirectiveArgument(raw, context);
34
+ if (!result.ok)
35
+ return { kind: "error", message: `meta name: ${result.message}`, code: result.code };
36
+ if (typeof result.value !== "string" || !result.value.trim())
37
+ return { kind: "error", message: `the meta name must be a non-empty string, got ${describeValue(result.value)}`, code: ErrorCodes.TYPE_ERROR };
38
+ return result.value;
39
+ }
40
+ function resolveMetaValue(raw, context) {
41
+ const literal = parseStringLiteral(raw);
42
+ if (literal !== null)
43
+ return literal;
44
+ const result = evaluateDirectiveArgument(raw, context);
45
+ if (!result.ok)
46
+ return { kind: "error", message: result.message, code: result.code };
47
+ const value = result.value;
48
+ if (value === void 0 || value === null || value === !1)
49
+ return "";
50
+ if (Array.isArray(value))
51
+ return value.filter((item) => item !== void 0 && item !== null && item !== "").map(String).join(", ");
52
+ if (typeof value === "object" && !(value instanceof Date))
53
+ return { kind: "error", message: `the content must be text, got ${describeValue(value)}`, code: ErrorCodes.TYPE_ERROR };
54
+ return String(value);
55
+ }
package/dist/process.js CHANGED
@@ -725,7 +725,7 @@ async function processOtherDirectives(template, context, filePath, options, depe
725
725
  context.__stx_head_raw = `${context.__stx_head_raw || ""}${headResult.headContent}
726
726
  `;
727
727
  output = processTitleDirective(output, context);
728
- output = processMetaDirective(output, context);
728
+ output = processMetaDirective(output, context, filePath);
729
729
  output = processRouteDirectives(output);
730
730
  output = processAuthDirectives(output, context);
731
731
  output = processCsrfDirectives(output);
package/dist/pwa.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
- import{Fk as wq,Gk as dq,Hk as Cq,Ik as mq,Jk as gq}from"./chunk-wvc7t11n.js";import{Kk as S,Lk as Tq,Mk as _q,Nk as z,Ok as D,Pk as P,Qk as kq,Rk as vq,Sk as w,Tk as Iq,Uk as d,Vk as Hq,Wk as Oq,Xk as Mq}from"./chunk-yzv702kk.js";import{ow as Aq,pw as bq,qw as Sq,rw as zq,sw as Dq,tw as Pq}from"./chunk-yr7camcd.js";import{xw as N}from"./chunk-p5d8sb50.js";import p from"fs";import E from"path";import C from"fs";import L from"path";var k=[72,96,128,144,152,192,384,512],v=[120,152,167,180],I=[16,32];async function H(G,B){let q={success:!0,generatedFiles:[],errors:[],warnings:[]},K=G.pwa?.icons;if(!K?.src)return q.errors.push("No source icon specified in pwa.icons.src"),q.success=!1,q;let Y=L.isAbsolute(K.src)?K.src:L.resolve(process.cwd(),K.src);if(!await Bun.file(Y).exists())return q.errors.push(`Source icon not found: ${Y}`),q.success=!1,q;let Q=L.resolve(B,K.outputDir||"pwa-icons");try{C.mkdirSync(Q,{recursive:!0})}catch{return q.errors.push(`Failed to create output directory: ${Q}`),q.success=!1,q}let X=K.sizes||k,j=K.generateWebP??!0,J=K.generateAppleIcons??!0;try{let y;try{y=await import("sharp")}catch{return q.errors.push(`sharp is not installed. Install it with: bun add sharp
3
- Icon generation requires the sharp package for image processing.`),q.success=!1,q}let R=await y.default(Y).metadata();if(R.width&&R.width<512)q.warnings.push(`Source icon is ${R.width}x${R.height}. Recommend using at least 512x512 for best quality.`);for(let $ of X){let x=L.join(Q,`icon-${$}x${$}.png`);if(await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x),j){let F=L.join(Q,`icon-${$}x${$}.webp`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).webp({quality:90}).toFile(F),q.generatedFiles.push(F)}}if(J){for(let x of v){let F=L.join(Q,`apple-touch-icon-${x}x${x}.png`);await y.default(Y).resize(x,x,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile(F),q.generatedFiles.push(F)}let $=L.join(Q,"apple-touch-icon.png");await y.default(Y).resize(180,180,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile($),q.generatedFiles.push($)}for(let $ of I){let x=L.join(Q,`favicon-${$}x${$}.png`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x)}let U=512,T=L.join(Q,`icon-maskable-${U}x${U}.png`),Z=Math.floor(U*0.1),_=U-Z*2;await y.default(Y).resize(_,_,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).extend({top:Z,bottom:Z,left:Z,right:Z,background:{r:255,g:255,b:255,alpha:1}}).png().toFile(T),q.generatedFiles.push(T)}catch(y){let V=y instanceof Error?y.message:String(y);q.errors.push(`Icon generation failed: ${V}`),q.success=!1}return q}function n(G){let B=G.sizes||k,q=G.outputDir?`/${G.outputDir}`:"/pwa-icons",K=G.generateWebP??!0,Y=G.generateAppleIcons??!0,Q=[];for(let X of B)if(Q.push(`${q}/icon-${X}x${X}.png`),K)Q.push(`${q}/icon-${X}x${X}.webp`);if(Y){Q.push(`${q}/apple-touch-icon.png`);for(let X of v)Q.push(`${q}/apple-touch-icon-${X}x${X}.png`)}for(let X of I)Q.push(`${q}/favicon-${X}x${X}.png`);return Q.push(`${q}/icon-maskable-512x512.png`),Q}async function O(){try{return await import("sharp"),!0}catch{return!1}}function m(G,B,q,K){let Y=[],Q=K.join(" ");for(let X of G)if(Y.push({src:`${B}/icon-${X}x${X}.png`,sizes:`${X}x${X}`,type:"image/png",purpose:Q}),q)Y.push({src:`${B}/icon-${X}x${X}.webp`,sizes:`${X}x${X}`,type:"image/webp",purpose:Q});return Y}function g(G){return G.map((B)=>({name:B.name,short_name:B.shortName,description:B.description,url:B.url,icons:B.icons?.map((q)=>({src:q.src,sizes:q.sizes,type:q.type}))}))}function h(G){let B=G.pwa?.shareTarget;if(!B?.enabled)return;let q={action:B.action||"/share",method:B.method||"POST",enctype:B.enctype||"multipart/form-data",params:{title:B.params?.title||"title",text:B.params?.text||"text",url:B.params?.url||"url"}};if(B.acceptFiles&&B.acceptFiles.length>0)q.params.files=B.acceptFiles.map((K)=>({name:K.name,accept:K.accept}));return q}function c(G){let B=G.pwa?.fileHandlers;if(!B?.enabled||!B.accept)return;let q=[{action:B.action||"/",accept:B.accept,launch_type:B.launchType||"single-client"}];if(B.icons)q[0].icons=B.icons.map((K)=>({src:K.src,sizes:K.sizes,type:K.type}));return q}function l(G){let B=G.pwa?.protocolHandlers;if(!B||B.length===0)return;return B.map((q)=>({protocol:q.protocol,url:q.url}))}function u(G){let B=G.pwa;if(!B?.enabled||!B.manifest)return null;let{manifest:q,icons:K}=B,Y=K?.sizes||[72,96,128,144,152,192,384,512],Q=K?.outputDir?`/${K.outputDir}`:"/pwa-icons",X=K?.generateWebP??!0,j=K?.purpose||["any","maskable"],J={name:q.name,short_name:q.shortName||q.name,description:q.description,start_url:q.startUrl||"/",display:q.display||"standalone",display_override:q.displayOverride,orientation:q.orientation,theme_color:q.themeColor,background_color:q.backgroundColor,scope:q.scope||"/",lang:q.lang,dir:q.dir,id:q.id,categories:q.categories,icons:m(Y,Q,X,j)};if(q.shortcuts&&q.shortcuts.length>0)J.shortcuts=g(q.shortcuts);if(q.screenshots&&q.screenshots.length>0)J.screenshots=q.screenshots.map((U)=>({src:U.src,sizes:U.sizes,type:U.type,label:U.label,form_factor:U.platform}));let y=h(G);if(y)J.share_target=y;let V=c(G);if(V)J.file_handlers=V;let R=l(G);if(R)J.protocol_handlers=R;if(q.launchHandler)J.launch_handler={client_mode:q.launchHandler};if(q.handleLinks)J.handle_links=q.handleLinks;if(q.edgeSidePanel?.enabled)J.edge_side_panel={preferred_width:q.edgeSidePanel.preferredWidth};return J}function M(G){let B=u(G);if(!B)return"";let q=JSON.parse(JSON.stringify(B));return JSON.stringify(q,null,2)}function i(G,B){return{...B,...G,shortcuts:G.shortcuts||B.shortcuts,screenshots:G.screenshots||B.screenshots,categories:G.categories||B.categories}}function s(G){let B=[],q=G.pwa;if(!q?.enabled)return B;let K=q.manifest;if(!K)return B.push("PWA manifest configuration is missing"),B;if(!K.name)B.push("Manifest name is required");let Y=["fullscreen","standalone","minimal-ui","browser"];if(K.display&&!Y.includes(K.display))B.push(`Invalid display mode: ${K.display}. Must be one of: ${Y.join(", ")}`);let Q=["any","natural","landscape","landscape-primary","landscape-secondary","portrait","portrait-primary","portrait-secondary"];if(K.orientation&&!Q.includes(K.orientation))B.push(`Invalid orientation: ${K.orientation}`);let X=/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;if(K.themeColor&&!X.test(K.themeColor))B.push(`Invalid theme color: ${K.themeColor}. Must be a valid hex color.`);if(K.backgroundColor&&!X.test(K.backgroundColor))B.push(`Invalid background color: ${K.backgroundColor}. Must be a valid hex color.`);if(q.shareTarget?.enabled){if(!q.shareTarget.action)B.push("Share target action URL is required")}if(q.fileHandlers?.enabled){if(!q.fileHandlers.accept||Object.keys(q.fileHandlers.accept).length===0)B.push("File handlers must specify accepted MIME types")}if(q.protocolHandlers)for(let j of q.protocolHandlers){if(!j.protocol)B.push("Protocol handler missing protocol");if(!j.url||!j.url.includes("%s"))B.push(`Protocol handler URL must contain %s placeholder: ${j.protocol}`)}return B}import A from"path";async function b(G){let B=G.pwa?.offline;if(!B?.enabled)return"";if(B.page){let q=A.isAbsolute(B.page)?B.page:A.resolve(process.cwd(),B.page);if(await Bun.file(q).exists())try{let{processDirectives:K}=await import("./chunk-rwa10jm4.js"),Y=await Bun.file(q).text();return await K(Y,{},q,G,new Set)}catch(K){console.warn(`[PWA] Failed to process custom offline page: ${K}`)}else console.warn(`[PWA] Custom offline page not found: ${q}`)}return f(G)}function f(G){let B=G.pwa?.offline,q=G.pwa?.manifest,K=B?.fallbackTitle||"You are offline",Y=B?.fallbackMessage||"Please check your internet connection and try again.",Q=q?.themeColor||"#4f46e5",X=q?.shortName||q?.name||"App";return`<!DOCTYPE html>
2
+ import{Fk as wq,Gk as dq,Hk as Cq,Ik as mq,Jk as gq}from"./chunk-wvc7t11n.js";import{Kk as S,Lk as Tq,Mk as _q,Nk as z,Ok as D,Pk as P,Qk as kq,Rk as vq,Sk as w,Tk as Iq,Uk as d,Vk as Hq,Wk as Oq,Xk as Mq}from"./chunk-c7qdnc5d.js";import{Aw as Sq,Bw as zq,Cw as Dq,Dw as Pq,yw as Aq,zw as bq}from"./chunk-s60eccqk.js";import{Hw as N}from"./chunk-0s3ry262.js";import p from"fs";import E from"path";import C from"fs";import L from"path";var k=[72,96,128,144,152,192,384,512],v=[120,152,167,180],I=[16,32];async function H(G,B){let q={success:!0,generatedFiles:[],errors:[],warnings:[]},K=G.pwa?.icons;if(!K?.src)return q.errors.push("No source icon specified in pwa.icons.src"),q.success=!1,q;let Y=L.isAbsolute(K.src)?K.src:L.resolve(process.cwd(),K.src);if(!await Bun.file(Y).exists())return q.errors.push(`Source icon not found: ${Y}`),q.success=!1,q;let Q=L.resolve(B,K.outputDir||"pwa-icons");try{C.mkdirSync(Q,{recursive:!0})}catch{return q.errors.push(`Failed to create output directory: ${Q}`),q.success=!1,q}let X=K.sizes||k,j=K.generateWebP??!0,J=K.generateAppleIcons??!0;try{let y;try{y=await import("sharp")}catch{return q.errors.push(`sharp is not installed. Install it with: bun add sharp
3
+ Icon generation requires the sharp package for image processing.`),q.success=!1,q}let R=await y.default(Y).metadata();if(R.width&&R.width<512)q.warnings.push(`Source icon is ${R.width}x${R.height}. Recommend using at least 512x512 for best quality.`);for(let $ of X){let x=L.join(Q,`icon-${$}x${$}.png`);if(await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x),j){let F=L.join(Q,`icon-${$}x${$}.webp`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).webp({quality:90}).toFile(F),q.generatedFiles.push(F)}}if(J){for(let x of v){let F=L.join(Q,`apple-touch-icon-${x}x${x}.png`);await y.default(Y).resize(x,x,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile(F),q.generatedFiles.push(F)}let $=L.join(Q,"apple-touch-icon.png");await y.default(Y).resize(180,180,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile($),q.generatedFiles.push($)}for(let $ of I){let x=L.join(Q,`favicon-${$}x${$}.png`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x)}let U=512,T=L.join(Q,`icon-maskable-${U}x${U}.png`),Z=Math.floor(U*0.1),_=U-Z*2;await y.default(Y).resize(_,_,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).extend({top:Z,bottom:Z,left:Z,right:Z,background:{r:255,g:255,b:255,alpha:1}}).png().toFile(T),q.generatedFiles.push(T)}catch(y){let V=y instanceof Error?y.message:String(y);q.errors.push(`Icon generation failed: ${V}`),q.success=!1}return q}function n(G){let B=G.sizes||k,q=G.outputDir?`/${G.outputDir}`:"/pwa-icons",K=G.generateWebP??!0,Y=G.generateAppleIcons??!0,Q=[];for(let X of B)if(Q.push(`${q}/icon-${X}x${X}.png`),K)Q.push(`${q}/icon-${X}x${X}.webp`);if(Y){Q.push(`${q}/apple-touch-icon.png`);for(let X of v)Q.push(`${q}/apple-touch-icon-${X}x${X}.png`)}for(let X of I)Q.push(`${q}/favicon-${X}x${X}.png`);return Q.push(`${q}/icon-maskable-512x512.png`),Q}async function O(){try{return await import("sharp"),!0}catch{return!1}}function m(G,B,q,K){let Y=[],Q=K.join(" ");for(let X of G)if(Y.push({src:`${B}/icon-${X}x${X}.png`,sizes:`${X}x${X}`,type:"image/png",purpose:Q}),q)Y.push({src:`${B}/icon-${X}x${X}.webp`,sizes:`${X}x${X}`,type:"image/webp",purpose:Q});return Y}function g(G){return G.map((B)=>({name:B.name,short_name:B.shortName,description:B.description,url:B.url,icons:B.icons?.map((q)=>({src:q.src,sizes:q.sizes,type:q.type}))}))}function h(G){let B=G.pwa?.shareTarget;if(!B?.enabled)return;let q={action:B.action||"/share",method:B.method||"POST",enctype:B.enctype||"multipart/form-data",params:{title:B.params?.title||"title",text:B.params?.text||"text",url:B.params?.url||"url"}};if(B.acceptFiles&&B.acceptFiles.length>0)q.params.files=B.acceptFiles.map((K)=>({name:K.name,accept:K.accept}));return q}function c(G){let B=G.pwa?.fileHandlers;if(!B?.enabled||!B.accept)return;let q=[{action:B.action||"/",accept:B.accept,launch_type:B.launchType||"single-client"}];if(B.icons)q[0].icons=B.icons.map((K)=>({src:K.src,sizes:K.sizes,type:K.type}));return q}function l(G){let B=G.pwa?.protocolHandlers;if(!B||B.length===0)return;return B.map((q)=>({protocol:q.protocol,url:q.url}))}function u(G){let B=G.pwa;if(!B?.enabled||!B.manifest)return null;let{manifest:q,icons:K}=B,Y=K?.sizes||[72,96,128,144,152,192,384,512],Q=K?.outputDir?`/${K.outputDir}`:"/pwa-icons",X=K?.generateWebP??!0,j=K?.purpose||["any","maskable"],J={name:q.name,short_name:q.shortName||q.name,description:q.description,start_url:q.startUrl||"/",display:q.display||"standalone",display_override:q.displayOverride,orientation:q.orientation,theme_color:q.themeColor,background_color:q.backgroundColor,scope:q.scope||"/",lang:q.lang,dir:q.dir,id:q.id,categories:q.categories,icons:m(Y,Q,X,j)};if(q.shortcuts&&q.shortcuts.length>0)J.shortcuts=g(q.shortcuts);if(q.screenshots&&q.screenshots.length>0)J.screenshots=q.screenshots.map((U)=>({src:U.src,sizes:U.sizes,type:U.type,label:U.label,form_factor:U.platform}));let y=h(G);if(y)J.share_target=y;let V=c(G);if(V)J.file_handlers=V;let R=l(G);if(R)J.protocol_handlers=R;if(q.launchHandler)J.launch_handler={client_mode:q.launchHandler};if(q.handleLinks)J.handle_links=q.handleLinks;if(q.edgeSidePanel?.enabled)J.edge_side_panel={preferred_width:q.edgeSidePanel.preferredWidth};return J}function M(G){let B=u(G);if(!B)return"";let q=JSON.parse(JSON.stringify(B));return JSON.stringify(q,null,2)}function i(G,B){return{...B,...G,shortcuts:G.shortcuts||B.shortcuts,screenshots:G.screenshots||B.screenshots,categories:G.categories||B.categories}}function s(G){let B=[],q=G.pwa;if(!q?.enabled)return B;let K=q.manifest;if(!K)return B.push("PWA manifest configuration is missing"),B;if(!K.name)B.push("Manifest name is required");let Y=["fullscreen","standalone","minimal-ui","browser"];if(K.display&&!Y.includes(K.display))B.push(`Invalid display mode: ${K.display}. Must be one of: ${Y.join(", ")}`);let Q=["any","natural","landscape","landscape-primary","landscape-secondary","portrait","portrait-primary","portrait-secondary"];if(K.orientation&&!Q.includes(K.orientation))B.push(`Invalid orientation: ${K.orientation}`);let X=/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;if(K.themeColor&&!X.test(K.themeColor))B.push(`Invalid theme color: ${K.themeColor}. Must be a valid hex color.`);if(K.backgroundColor&&!X.test(K.backgroundColor))B.push(`Invalid background color: ${K.backgroundColor}. Must be a valid hex color.`);if(q.shareTarget?.enabled){if(!q.shareTarget.action)B.push("Share target action URL is required")}if(q.fileHandlers?.enabled){if(!q.fileHandlers.accept||Object.keys(q.fileHandlers.accept).length===0)B.push("File handlers must specify accepted MIME types")}if(q.protocolHandlers)for(let j of q.protocolHandlers){if(!j.protocol)B.push("Protocol handler missing protocol");if(!j.url||!j.url.includes("%s"))B.push(`Protocol handler URL must contain %s placeholder: ${j.protocol}`)}return B}import A from"path";async function b(G){let B=G.pwa?.offline;if(!B?.enabled)return"";if(B.page){let q=A.isAbsolute(B.page)?B.page:A.resolve(process.cwd(),B.page);if(await Bun.file(q).exists())try{let{processDirectives:K}=await import("./chunk-fytemc96.js"),Y=await Bun.file(q).text();return await K(Y,{},q,G,new Set)}catch(K){console.warn(`[PWA] Failed to process custom offline page: ${K}`)}else console.warn(`[PWA] Custom offline page not found: ${q}`)}return f(G)}function f(G){let B=G.pwa?.offline,q=G.pwa?.manifest,K=B?.fallbackTitle||"You are offline",Y=B?.fallbackMessage||"Please check your internet connection and try again.",Q=q?.themeColor||"#4f46e5",X=q?.shortName||q?.name||"App";return`<!DOCTYPE html>
4
4
  <html lang="en">
5
5
  <head>
6
6
  <meta charset="UTF-8">
package/dist/seo.d.ts CHANGED
@@ -2,10 +2,15 @@
2
2
  * SEO (Search Engine Optimization) Module
3
3
  *
4
4
  * Provides directives and utilities for generating SEO-related HTML:
5
- * - `@meta('name', 'content')` - Generate meta tags
5
+ * - `@meta('name', 'content')` / `@meta('name', expression)` - Generate meta tags
6
6
  * - `@metaTag({ name, property, content })` - Meta tags with full control
7
- * - `@structuredData({ ... })` - JSON-LD structured data
8
- * - `@seo({ title, description, ... })` - Comprehensive SEO generation
7
+ * - `@structuredData({ ... })` / `@structuredData(expression)` - JSON-LD structured data
8
+ * - `@seo({ title, description, ... })` / `@seo(expression)` - Comprehensive SEO generation
9
+ *
10
+ * Every argument is an expression evaluated against the template's context, so
11
+ * a variable, a spread or a function call works wherever a literal does. A
12
+ * directive that cannot produce its tag says so: an inline comment where the
13
+ * tag would have been and a console warning naming the file.
9
14
  *
10
15
  * Also provides:
11
16
  * - Automatic SEO tag injection via `injectSeoTags()`
@@ -56,21 +61,36 @@
56
61
  */
57
62
  import type { CustomDirective, StxOptions } from './types';
58
63
  /**
59
- * Process @meta directive for generating meta tags.
60
- * Supports both simple `@meta('name', 'content')` and
61
- * OpenGraph `@meta('og:title')` formats.
64
+ * Process `@meta(...)` and `@metaTag({...})` in place, emitting the tags where
65
+ * the directives sit.
66
+ *
67
+ * This is the pass a partial's directives go through (a head partial is inside
68
+ * `<head>` already), and the page-level catch-all for any `@meta` the staging
69
+ * directive did not take. Arguments are resolved by `resolveMetaArguments`:
70
+ * `@meta('author', 'Jane')`, `@meta('author', author)`,
71
+ * `@meta('og:title', post.title)`, and the one-argument `@meta('og:title')`.
62
72
  */
63
- export declare function processMetaDirectives(template: string, context: Record<string, any>, _filePath: string, _options: StxOptions): string;
73
+ export declare function processMetaDirectives(template: string, context: Record<string, any>, filePath: string, _options: StxOptions): string;
64
74
  /**
65
- * Process @structuredData directive for JSON-LD.
66
- * Automatically adds schema.org context if not provided.
75
+ * Process `@structuredData(...)` for JSON-LD.
76
+ *
77
+ * The argument is any expression that evaluates to an object, or an array of
78
+ * objects: `@structuredData({ '@type': 'Product', name })`,
79
+ * `@structuredData(product)`, `@structuredData(buildSchema(post))`. schema.org
80
+ * is filled in as the `@context` when the data has none.
67
81
  */
68
- export declare function processStructuredData(template: string, context: Record<string, any>, _filePath: string): string;
82
+ export declare function processStructuredData(template: string, context: Record<string, any>, filePath: string): string;
69
83
  /**
70
- * Process @seo directive for automatic meta tag generation.
71
- * Generates title, description, Open Graph, Twitter, and structured data tags.
84
+ * Process `@seo(...)` for title, description, Open Graph, Twitter and
85
+ * structured data tags.
86
+ *
87
+ * The argument is any expression that evaluates to an object:
88
+ * `@seo({ title, description })`, `@seo(seo)`, `@seo({ ...seo, title })`,
89
+ * `@seo(buildSeo(page))`. `null` or `undefined` from a variable that exists
90
+ * renders nothing; a variable that does not exist, a value that is not an
91
+ * object, and keys `@seo` does not read are all reported.
72
92
  */
73
- export declare function processSeoDirective(template: string, context: Record<string, any>, _filePath: string, _options: StxOptions): string;
93
+ export declare function processSeoDirective(template: string, context: Record<string, any>, filePath: string, _options: StxOptions): string;
74
94
  /**
75
95
  * Injects default SEO tags if no @seo directive is used.
76
96
  * Respects seo.enabled config and skipDefaultSeoTags option.
@@ -87,13 +107,15 @@ export declare function injectSeoTags(html: string, context: Record<string, any>
87
107
  *
88
108
  * This directive shadows `processMetaDirective` in head.ts, since custom
89
109
  * directives run first. It therefore has to do that function's job, which is
90
- * why it resolves unquoted arguments from the context and separates `property`
91
- * from `name` here rather than leaving it to a later pass that will never see
92
- * the directive.
110
+ * why it takes its arguments as written (`rawParams`) and resolves them with
111
+ * the same `resolveMetaArguments` the other two @meta passes use: a quoted
112
+ * value is text, anything else is an expression.
93
113
  */
94
114
  export declare const metaDirective: CustomDirective;
95
115
  /**
96
- * SEO structured data directive for JSON-LD generation
116
+ * SEO structured data directive for JSON-LD generation, the block form:
117
+ * `@structuredData { ... } @endstructuredData`. The body is JSON, not an
118
+ * expression; `@structuredData(expression)` is the form that reads variables.
97
119
  */
98
120
  export declare const structuredDataDirective: CustomDirective;
99
121
  /**