@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.
- package/dist/caching.js +1 -0
- package/dist/{chunk-48x5bsqv.js → chunk-0dpgh7a6.js} +1 -1
- package/dist/{chunk-evtrctj9.js → chunk-0fp16c1x.js} +1 -1
- package/dist/{chunk-p5d8sb50.js → chunk-0s3ry262.js} +1 -1
- package/dist/{chunk-tmj45xq5.js → chunk-12qteqk8.js} +1 -1
- package/dist/chunk-160g7ynw.js +2 -0
- package/dist/chunk-16hr5y08.js +2 -0
- package/dist/{chunk-anf2y2h5.js → chunk-1bfvdjpy.js} +1 -1
- package/dist/{chunk-y956wapy.js → chunk-1h1c2048.js} +1 -1
- package/dist/{chunk-bt8w146r.js → chunk-1qtd4ex2.js} +2 -2
- package/dist/{chunk-2swnc8kg.js → chunk-1zav2sf1.js} +1 -1
- package/dist/{chunk-gbkq5qnw.js → chunk-2853ye06.js} +2 -2
- package/dist/{chunk-4vcyf1kh.js → chunk-36j9qset.js} +2 -2
- package/dist/{chunk-74r1zw2h.js → chunk-3d3x7dwy.js} +1 -1
- package/dist/chunk-3h97y3n3.js +2 -0
- package/dist/chunk-3j915h2d.js +2 -0
- package/dist/{chunk-1gwbw81h.js → chunk-45ydb29q.js} +1 -1
- package/dist/{chunk-ts3wfjh9.js → chunk-4fafxr8d.js} +1 -1
- package/dist/chunk-4hkg6kx4.js +2 -0
- package/dist/{chunk-8tsr51hj.js → chunk-4pg6wbt6.js} +1 -1
- package/dist/{chunk-saqbtgqr.js → chunk-4vd0rqfs.js} +1 -1
- package/dist/{chunk-wv8h3d68.js → chunk-53sjbp3z.js} +1 -1
- package/dist/chunk-57494khk.js +2 -0
- package/dist/{chunk-4wpxbkbe.js → chunk-57dxdvjh.js} +1 -1
- package/dist/{chunk-k3w3gwm3.js → chunk-5azeswj2.js} +1 -1
- package/dist/{chunk-k5b9471y.js → chunk-5hq25yky.js} +1 -1
- package/dist/chunk-5xavmr4t.js +2 -0
- package/dist/{chunk-kznb3hdj.js → chunk-5zemhhwe.js} +1 -1
- package/dist/{chunk-ghd80w8a.js → chunk-62vng8g8.js} +2 -2
- package/dist/{chunk-reskzmsb.js → chunk-649dfqqz.js} +1 -1
- package/dist/{chunk-6nxq8rcw.js → chunk-6g8rvnhc.js} +1 -1
- package/dist/chunk-6rsfkms1.js +2 -0
- package/dist/{chunk-xe93ae3r.js → chunk-6xs80tka.js} +3 -3
- package/dist/{chunk-s4mmmwj4.js → chunk-7mc1k1tq.js} +1 -1
- package/dist/{chunk-25b8z7xa.js → chunk-7r79j1hs.js} +1 -1
- package/dist/{chunk-drs93sry.js → chunk-7yfk9393.js} +1 -1
- package/dist/{chunk-r46cg4qa.js → chunk-7zxv2631.js} +1 -1
- package/dist/{chunk-d389vyyz.js → chunk-8ah5mhst.js} +2 -2
- package/dist/{chunk-zv4gb3wb.js → chunk-905j49fe.js} +1 -1
- package/dist/chunk-9czj483k.js +2 -0
- package/dist/chunk-9kv0qavn.js +2 -0
- package/dist/{chunk-m39hb2a2.js → chunk-a0ajg9z5.js} +1 -1
- package/dist/{chunk-mznr9nyp.js → chunk-ag8gdk51.js} +1 -1
- package/dist/{chunk-gwc54hjc.js → chunk-amfq5rnz.js} +2 -2
- package/dist/{chunk-rh7mwwb4.js → chunk-aqxwwq6w.js} +2 -2
- package/dist/{chunk-hk8j3eps.js → chunk-ay336sqb.js} +1 -1
- package/dist/chunk-bcz8yp2g.js +2 -0
- package/dist/{chunk-rr49zrz6.js → chunk-brjfzbq9.js} +1 -1
- package/dist/chunk-bxefsc7k.js +2 -0
- package/dist/chunk-c4kv3r8t.js +2 -0
- package/dist/{chunk-yzv702kk.js → chunk-c7qdnc5d.js} +1 -1
- package/dist/{chunk-ys56ecvx.js → chunk-daxr6p6q.js} +1 -1
- package/dist/{chunk-yhvrq2qd.js → chunk-dj5mqnp1.js} +1 -1
- package/dist/{chunk-da0tfpa2.js → chunk-e53pwshq.js} +1 -1
- package/dist/{chunk-k9ep24bd.js → chunk-e6rxvz3p.js} +1 -1
- package/dist/{chunk-x7dv5rq7.js → chunk-ec18ftcg.js} +1 -1
- package/dist/chunk-eh3meyd3.js +64 -0
- package/dist/{chunk-yq7fc8sa.js → chunk-em8cr7wy.js} +1 -1
- package/dist/{chunk-thrv2nbz.js → chunk-ey1tsk22.js} +1 -1
- package/dist/{chunk-e0kzyf1g.js → chunk-fbmjw0bh.js} +1 -1
- package/dist/chunk-fytemc96.js +2 -0
- package/dist/{chunk-zv4nf0x2.js → chunk-g0dbtyk7.js} +1 -1
- package/dist/chunk-g7gc0ten.js +2 -0
- package/dist/chunk-gqsxxd81.js +2 -0
- package/dist/{chunk-sytwy0mg.js → chunk-grnmr3y1.js} +1 -1
- package/dist/{chunk-4ecv1e3w.js → chunk-gvfgrve8.js} +1 -1
- package/dist/chunk-gx9z0wmm.js +2 -0
- package/dist/{chunk-3b0mgxbk.js → chunk-h6y99nak.js} +1 -1
- package/dist/chunk-hq3phk1a.js +3 -0
- package/dist/{chunk-01ggk8k8.js → chunk-hrdd26r4.js} +1 -1
- package/dist/{chunk-567g1ttv.js → chunk-hzbty7hz.js} +1 -1
- package/dist/{chunk-6nvm939g.js → chunk-jagf7h9y.js} +1 -1
- package/dist/{chunk-xec226zb.js → chunk-jh6f1a9y.js} +1 -1
- package/dist/chunk-jnxzzzm4.js +2 -0
- package/dist/chunk-jy6jwxhs.js +2 -0
- package/dist/{chunk-r1rfwm01.js → chunk-k3dtvay1.js} +1 -1
- package/dist/chunk-k8skvm0d.js +2 -0
- package/dist/{chunk-1xwfws06.js → chunk-kbah2k8e.js} +2 -2
- package/dist/{chunk-beextc84.js → chunk-kgbz86sa.js} +2 -2
- package/dist/chunk-khqn40gv.js +2 -0
- package/dist/{chunk-fx29n4vc.js → chunk-m0xaw0qw.js} +1 -1
- package/dist/chunk-mhbzrh5y.js +2 -0
- package/dist/{chunk-mbstfjnj.js → chunk-mwvxhv0r.js} +1 -1
- package/dist/{chunk-cqw3c1xg.js → chunk-p3awq80d.js} +1 -1
- package/dist/chunk-p8anntjb.js +2 -0
- package/dist/chunk-pwe51myj.js +2 -0
- package/dist/{chunk-gkq323fg.js → chunk-pwhf5kte.js} +1 -1
- package/dist/{chunk-25f0s8t1.js → chunk-q6825wwz.js} +1 -1
- package/dist/{chunk-f2qbjr3z.js → chunk-q9ckfwhh.js} +1 -1
- package/dist/{chunk-ytx17bg2.js → chunk-rns7wjy9.js} +1 -1
- package/dist/chunk-rqqdvnp5.js +2 -0
- package/dist/chunk-rzg9pars.js +7 -0
- package/dist/{chunk-yr7camcd.js → chunk-s60eccqk.js} +1 -1
- package/dist/{chunk-ntm1761b.js → chunk-s7qbba8r.js} +10 -10
- package/dist/{chunk-hrmxxs0w.js → chunk-safmgrq5.js} +1 -1
- package/dist/{chunk-ebd3z4j9.js → chunk-stqm4brp.js} +1 -1
- package/dist/{chunk-1t3j8a0q.js → chunk-t00ga194.js} +1 -1
- package/dist/{chunk-fm5211ec.js → chunk-tqw1yean.js} +1 -1
- package/dist/{chunk-hnffkdzc.js → chunk-tz68qv47.js} +1 -1
- package/dist/{chunk-1spzqjve.js → chunk-vaggjvsm.js} +1 -1
- package/dist/{chunk-q4cemvkw.js → chunk-vj3nx26v.js} +1 -1
- package/dist/chunk-w1mbrq1g.js +2 -0
- package/dist/chunk-w84qe2m6.js +2 -0
- package/dist/{chunk-c33y19bn.js → chunk-wdaf98ya.js} +1 -1
- package/dist/chunk-wjm7hv5e.js +2 -0
- package/dist/{chunk-byjy9far.js → chunk-wnmegbw0.js} +1 -1
- package/dist/{chunk-0sq5mdrh.js → chunk-wyhb3nb0.js} +1 -1
- package/dist/{chunk-j82hfkcf.js → chunk-xgw269cf.js} +234 -236
- package/dist/{chunk-jcbj3xp4.js → chunk-y5sxa2sq.js} +1 -1
- package/dist/{chunk-mkgbrbr9.js → chunk-yefnrr5z.js} +1 -1
- package/dist/chunk-yr7s02f7.js +2 -0
- package/dist/{chunk-p52cm7ch.js → chunk-z9hb80mk.js} +1 -1
- package/dist/{chunk-c73pyeak.js → chunk-zcytk5w9.js} +1 -1
- package/dist/chunk-zq3tm2c2.js +2 -0
- package/dist/{chunk-5g9vbhbt.js → chunk-zr1x54bt.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/craft-entry.js +1 -1
- package/dist/craft.js +1 -1
- package/dist/custom-directives.js +21 -8
- package/dist/database.js +1 -1
- package/dist/directive-arguments.d.ts +107 -0
- package/dist/directive-arguments.js +171 -0
- package/dist/head.d.ts +7 -30
- package/dist/head.js +15 -5
- package/dist/meta-arguments.d.ts +52 -0
- package/dist/meta-arguments.js +55 -0
- package/dist/process.js +1 -1
- package/dist/pwa.js +2 -2
- package/dist/seo.d.ts +39 -17
- package/dist/seo.js +221 -255
- package/dist/ssg.js +2 -2
- package/dist/types/directive-types.d.ts +12 -0
- package/dist/visual-testing.js +1 -1
- package/package.json +5 -5
- package/dist/chunk-025z2btp.js +0 -2
- package/dist/chunk-19fp0z9s.js +0 -2
- package/dist/chunk-23sv24cm.js +0 -2
- package/dist/chunk-2p68bkza.js +0 -2
- package/dist/chunk-5grrqaqs.js +0 -2
- package/dist/chunk-6jx4r94x.js +0 -2
- package/dist/chunk-6p6b01kp.js +0 -4
- package/dist/chunk-a1rt1fms.js +0 -2
- package/dist/chunk-ahpmjg3f.js +0 -64
- package/dist/chunk-akp89n3w.js +0 -2
- package/dist/chunk-bqhrryqs.js +0 -2
- package/dist/chunk-czvzaa42.js +0 -3
- package/dist/chunk-de2cfcf5.js +0 -2
- package/dist/chunk-exhbc57n.js +0 -2
- package/dist/chunk-fhhp6pmx.js +0 -2
- package/dist/chunk-fqsrft0a.js +0 -2
- package/dist/chunk-gqzccw30.js +0 -2
- package/dist/chunk-h47e1c03.js +0 -2
- package/dist/chunk-kbemcvp1.js +0 -2
- package/dist/chunk-ks4j8fjz.js +0 -2
- package/dist/chunk-pbct6nxk.js +0 -2
- package/dist/chunk-peerbcwj.js +0 -2
- package/dist/chunk-pt1w6vq2.js +0 -2
- package/dist/chunk-rgda0gwf.js +0 -2
- package/dist/chunk-rwa10jm4.js +0 -2
- package/dist/chunk-snsh9ye4.js +0 -2
- package/dist/chunk-td39mtts.js +0 -2
- package/dist/chunk-trncnwem.js +0 -2
- package/dist/chunk-vk2j3vvs.js +0 -2
- package/dist/chunk-vph6zpeq.js +0 -2
- package/dist/chunk-wa3ks5zv.js +0 -2
- package/dist/chunk-xer5kn26.js +0 -2
- 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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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-
|
|
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-
|
|
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
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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>,
|
|
73
|
+
export declare function processMetaDirectives(template: string, context: Record<string, any>, filePath: string, _options: StxOptions): string;
|
|
64
74
|
/**
|
|
65
|
-
* Process
|
|
66
|
-
*
|
|
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>,
|
|
82
|
+
export declare function processStructuredData(template: string, context: Record<string, any>, filePath: string): string;
|
|
69
83
|
/**
|
|
70
|
-
* Process
|
|
71
|
-
*
|
|
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>,
|
|
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
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
/**
|