@stacksjs/stx 0.2.175 → 0.2.177

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 (59) hide show
  1. package/dist/a11y.d.ts +72 -2
  2. package/dist/a11y.js +25 -10
  3. package/dist/builtins/attrs.d.ts +19 -0
  4. package/dist/builtins/attrs.js +18 -0
  5. package/dist/builtins/safe-image.js +2 -1
  6. package/dist/builtins/stx-image.js +2 -1
  7. package/dist/builtins/stx-link.js +2 -1
  8. package/dist/cli-flags.d.ts +34 -0
  9. package/dist/cli-flags.js +20 -0
  10. package/dist/cli.js +1132 -895
  11. package/dist/client-script.js +2 -1
  12. package/dist/component-registry.d.ts +9 -0
  13. package/dist/component-renderer.js +20 -1
  14. package/dist/composables/use-fetch.d.ts +6 -1
  15. package/dist/composables/use-fetch.js +40 -18
  16. package/dist/composables/use-query.d.ts +7 -2
  17. package/dist/cookie-serialize.d.ts +40 -0
  18. package/dist/cookie-serialize.js +18 -0
  19. package/dist/craft.js +670 -442
  20. package/dist/crosswind-config.d.ts +13 -0
  21. package/dist/crosswind-config.js +6 -2
  22. package/dist/dev-server/crosswind.js +2 -1
  23. package/dist/edge-runtime.d.ts +2 -9
  24. package/dist/edge-runtime.js +2 -16
  25. package/dist/includes.js +8 -4
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +3 -0
  28. package/dist/page-action.d.ts +10 -20
  29. package/dist/page-action.js +33 -9
  30. package/dist/process.js +2 -1
  31. package/dist/production-server.js +8 -7
  32. package/dist/props.d.ts +51 -8
  33. package/dist/pwa.js +637 -409
  34. package/dist/reactive.js +2 -2
  35. package/dist/resolve-stx-targets.d.ts +49 -0
  36. package/dist/resolve-stx-targets.js +38 -0
  37. package/dist/runtime-globals.d.ts +24 -0
  38. package/dist/runtime-globals.js +16 -0
  39. package/dist/runtime-injection.d.ts +36 -0
  40. package/dist/runtime-injection.js +14 -0
  41. package/dist/server-bindings.js +3 -0
  42. package/dist/signal-processing.js +38 -7
  43. package/dist/signals.js +157 -35
  44. package/dist/ssg.d.ts +23 -0
  45. package/dist/ssg.js +615 -383
  46. package/dist/ssr.js +2 -16
  47. package/dist/stx-virtual-ts.d.ts +72 -4
  48. package/dist/stx-virtual-ts.js +111 -5
  49. package/dist/stx.d.ts +66 -7
  50. package/dist/template-hydrator.d.ts +1 -1
  51. package/dist/template-hydrator.js +6 -6
  52. package/dist/theme-tokens.d.ts +88 -0
  53. package/dist/theme-tokens.js +71 -0
  54. package/dist/typecheck.d.ts +6 -1
  55. package/dist/typecheck.js +102 -67
  56. package/dist/types/config-types.d.ts +24 -0
  57. package/dist/utils.js +5 -4
  58. package/package.json +5 -5
  59. package/stx.d.ts +66 -7
package/dist/a11y.d.ts CHANGED
@@ -36,6 +36,29 @@ export interface A11yViolation {
36
36
  /** URL to more information */
37
37
  helpUrl?: string;
38
38
  }
39
+ /**
40
+ * Has this element declared itself decorative rather than semantic?
41
+ *
42
+ * `role="presentation"` and its synonym `role="none"` remove an element's
43
+ * implicit semantics from the accessibility tree. On a table that is exactly the
44
+ * declaration that it carries layout, not data — so demanding a `<th>` or a
45
+ * `<caption>` asks the author to reintroduce the semantics the role exists to
46
+ * remove (stacksjs/stx#1916).
47
+ *
48
+ * This is not a corner case. Every HTML email is nested layout tables: mail
49
+ * clients strip `<style>` and have no reliable box model, so `role="presentation"`
50
+ * is the standard, correct pattern there. One email template accounted for every
51
+ * remaining finding in an otherwise-clean app, and none of them was fixable —
52
+ * the correct markup was what triggered them. An advisory checker with a
53
+ * permanent finding nobody can close is one people learn to ignore, which costs
54
+ * more than the rule is worth.
55
+ *
56
+ * The rule itself is right to exist: a DATA table with no headers is a real
57
+ * problem. It just has to take the author's word for which kind it is.
58
+ */
59
+ export declare function isPresentationalRole(role: string | null | undefined): boolean;
60
+ /** Read one attribute out of a raw tag's attribute text. */
61
+ export declare function attributeValue(attrs: string, name: string): string | null;
39
62
  /**
40
63
  * Process accessibility directives.
41
64
  * Handles @a11y hints, @screenReader content, and @ariaDescribe.
@@ -63,6 +86,23 @@ else {
63
86
  * ```
64
87
  */
65
88
  export declare function hasDOMSupport(): boolean;
89
+ /**
90
+ * Regex-based accessibility checker for non-DOM environments.
91
+ * Provides a subset of checks that can be performed with regex parsing.
92
+ *
93
+ * @param html - HTML content to check
94
+ * @param _filePath - File path for error reporting
95
+ * @returns Array of accessibility violations found
96
+ */
97
+ /**
98
+ * Regex-based checking, used when no DOM is available — which is the CLI.
99
+ *
100
+ * Exported so tests can exercise it directly. It and the DOM path implement the
101
+ * same rules twice, and the way two implementations of one rule go wrong is by
102
+ * disagreeing; pinning only whichever one the test environment happens to pick
103
+ * leaves the other free to drift (#1916).
104
+ */
105
+ export declare function checkA11yWithRegex(html: string, _filePath: string): A11yViolation[];
66
106
  /**
67
107
  * Automatically check template for accessibility issues.
68
108
  *
@@ -81,9 +121,39 @@ export declare function hasDOMSupport(): boolean;
81
121
  * ```
82
122
  */
83
123
  export declare function checkA11y(html: string, filePath: string): Promise<A11yViolation[]>;
124
+ /** What a scan actually read, alongside what it found. */
125
+ export interface A11yScanResult {
126
+ /** Files that had at least one violation, mapped to their violations. */
127
+ results: Record<string, A11yViolation[]>;
128
+ /**
129
+ * Every file the scan READ.
130
+ *
131
+ * Reported separately because "no violations" and "no files" are different
132
+ * answers that used to be the same one: `results` only ever contains files
133
+ * WITH violations, so an empty map means both "everything is clean" and
134
+ * "nothing was scanned", and the success banner was printed for both
135
+ * (stacksjs/stx#1918).
136
+ */
137
+ scannedFiles: string[];
138
+ }
139
+ /**
140
+ * Scan a directory, a file, or a glob for accessibility issues.
141
+ *
142
+ * The argument used to be joined into a scan pattern, so only a directory
143
+ * worked: a file path became `<file>/**` + `/*.stx` and a glob became a doubled
144
+ * pattern, both matching nothing and both reported as a clean pass. See
145
+ * `resolve-stx-targets.ts` for what that cost.
146
+ */
147
+ export declare function scanA11yTargets(target: string | readonly string[], options?: {
148
+ recursive?: boolean;
149
+ ignorePaths?: string[];
150
+ }): Promise<A11yScanResult>;
84
151
  /**
85
- * Scan a directory of stx files for accessibility issues.
86
- * Returns a map of file paths to their violations.
152
+ * Back-compatible wrapper: violations only.
153
+ *
154
+ * Kept so existing callers keep working, but it cannot say whether the scan read
155
+ * anything — which is the distinction #1918 turned on. Prefer
156
+ * {@link scanA11yTargets}.
87
157
  */
88
158
  export declare function scanA11yIssues(directory: string, options?: {
89
159
  recursive?: boolean;
package/dist/a11y.js CHANGED
@@ -1,4 +1,16 @@
1
1
  import path from "node:path";
2
+ import { resolveStxTargets } from "./resolve-stx-targets.js";
3
+ export function isPresentationalRole(role) {
4
+ if (!role)
5
+ return !1;
6
+ return role.trim().split(/\s+/)[0]?.toLowerCase() === "presentation" || role.trim().split(/\s+/)[0]?.toLowerCase() === "none";
7
+ }
8
+ export function attributeValue(attrs, name) {
9
+ const match = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs);
10
+ if (!match)
11
+ return null;
12
+ return match[2] ?? match[3] ?? match[4] ?? null;
13
+ }
2
14
  export function processA11yDirectives(template, _context, _filePath, _options) {
3
15
  let output = template;
4
16
  output = output.replace(/@a11y\(\s*['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"]\s*)?\)/g, (_, hintType, customMessage) => {
@@ -58,7 +70,7 @@ function protectStxComponentTags(source) {
58
70
  })
59
71
  };
60
72
  }
61
- function checkA11yWithRegex(html, _filePath) {
73
+ export function checkA11yWithRegex(html, _filePath) {
62
74
  const violations = [], imgRegex = /<img\s+([^>]*)>/gi;
63
75
  let match;
64
76
  match = imgRegex.exec(html);
@@ -156,11 +168,11 @@ function checkA11yWithRegex(html, _filePath) {
156
168
  help: "Add lang attribute to the html element",
157
169
  helpUrl: "https://web.dev/learn/accessibility/more-html/#language"
158
170
  });
159
- const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
171
+ const tableRegex = /<table([^>]*)>([\s\S]*?)<\/table>/gi;
160
172
  match = tableRegex.exec(html);
161
173
  while (match !== null) {
162
- const tableContent = match[1];
163
- if (!/<th\b/i.test(tableContent))
174
+ const tableContent = match[2];
175
+ if (!isPresentationalRole(attributeValue(match[1], "role")) && !/<th\b/i.test(tableContent))
164
176
  violations.push({
165
177
  type: "table-missing-headers",
166
178
  element: "<table>",
@@ -321,6 +333,8 @@ export async function checkA11y(html, filePath) {
321
333
  });
322
334
  }
323
335
  container.querySelectorAll("table").forEach((table) => {
336
+ if (isPresentationalRole(table.getAttribute("role")))
337
+ return;
324
338
  const hasCaption = table.querySelector("caption");
325
339
  if (!table.querySelector("th"))
326
340
  violations.push({
@@ -411,23 +425,24 @@ export async function checkA11y(html, filePath) {
411
425
  }
412
426
  return violations;
413
427
  }
414
- export async function scanA11yIssues(directory, options = {}) {
415
- const results = {}, { recursive = !0, ignorePaths = [] } = options, pattern = path.join(directory, recursive ? "**/*.stx" : "*.stx"), glob = new Bun.Glob(pattern), files = [];
416
- for await (const file of glob.scan())
417
- if (!ignorePaths.some((ignorePath) => file.includes(path.normalize(ignorePath))))
418
- files.push(file);
428
+ export async function scanA11yTargets(target, options = {}) {
429
+ const results = {}, { recursive = !0, ignorePaths = [] } = options, targets = Array.isArray(target) ? target : [target], files = await resolveStxTargets(targets, { recursive, ignorePaths }), scannedFiles = [];
419
430
  for (const file of files)
420
431
  try {
421
432
  const content = await Bun.file(file).text();
422
433
  if (file.endsWith("good-a11y.stx"))
423
434
  continue;
424
435
  const violations = await checkA11y(content, file);
436
+ scannedFiles.push(file);
425
437
  if (violations.length > 0)
426
438
  results[file] = violations;
427
439
  } catch (error) {
428
440
  console.error(`Error scanning ${file}:`, error);
429
441
  }
430
- return results;
442
+ return { results, scannedFiles };
443
+ }
444
+ export async function scanA11yIssues(directory, options = {}) {
445
+ return (await scanA11yTargets(directory, options)).results;
431
446
  }
432
447
  const defaultAutoFixConfig = {
433
448
  fixMissingAlt: !0,
@@ -28,3 +28,22 @@ import type { ResolvedProps } from '../component-registry';
28
28
  * @param consumed - Prop names the builtin has already rendered itself.
29
29
  */
30
30
  export declare function forwardStaticAttrs(props: ResolvedProps, consumed: Set<string>): string[];
31
+ /**
32
+ * Render the server-evaluated `:prop` bindings a builtin did not consume.
33
+ *
34
+ * Only `static` was ever forwarded, so `<StxLink to="/x" :title="pageTitle">`
35
+ * emitted an `<a>` with no title and no warning — the binding was resolved,
36
+ * stored, and thrown away (stacksjs/stx#1930). Client-reactive `:prop` and
37
+ * `@event` bindings already reach the element by another route (the component
38
+ * renderer re-applies them to the root), which is what made the gap specific to
39
+ * the server-evaluated ones and easy to miss.
40
+ *
41
+ * `null`, `undefined` and `false` DROP the attribute rather than rendering the
42
+ * word — that is what makes `:aria-current="active ? 'page' : null"` express
43
+ * "this attribute is conditional", which is the whole reason to reach for a
44
+ * binding instead of a literal. `true` renders bare, as HTML booleans do.
45
+ *
46
+ * @param props - The component's resolved props.
47
+ * @param consumed - Prop names the builtin has already rendered itself.
48
+ */
49
+ export declare function forwardResolvedAttrs(props: ResolvedProps, consumed: Set<string>): string[];
@@ -19,3 +19,21 @@ export function forwardStaticAttrs(props, consumed) {
19
19
  }
20
20
  return attrs;
21
21
  }
22
+ export function forwardResolvedAttrs(props, consumed) {
23
+ const attrs = [];
24
+ for (const [key, value] of Object.entries(props.serverDynamic ?? {})) {
25
+ if (consumed.has(key))
26
+ continue;
27
+ if (value === null || value === void 0 || value === !1)
28
+ continue;
29
+ const name = props.serverDynamicNames?.[key] ?? key;
30
+ if (value === !0) {
31
+ attrs.push(escapeAttr(name));
32
+ continue;
33
+ }
34
+ if (typeof value === "object")
35
+ continue;
36
+ attrs.push(`${escapeAttr(name)}="${escapeAttr(stripControlChars(String(value)))}"`);
37
+ }
38
+ return attrs;
39
+ }
@@ -1,4 +1,4 @@
1
- import { forwardStaticAttrs } from "./attrs.js";
1
+ import { forwardResolvedAttrs, forwardStaticAttrs } from "./attrs.js";
2
2
  const DEFAULT_FALLBACK = "/images/safe-image-default.svg";
3
3
  function escapeAttr(value) {
4
4
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -34,6 +34,7 @@ export const SafeImageBuiltin = {
34
34
  "fallbackClassName"
35
35
  ]);
36
36
  imgAttrs.push(...forwardStaticAttrs(props, consumed));
37
+ imgAttrs.push(...forwardResolvedAttrs(props, consumed));
37
38
  const imgTag = `<img ${imgAttrs.join(" ")} />`, renderCtx = ctx.context;
38
39
  if (!renderCtx.__safeImageHandlerEmitted) {
39
40
  renderCtx.__safeImageHandlerEmitted = !0;
@@ -1,4 +1,4 @@
1
- import { forwardStaticAttrs } from "./attrs.js";
1
+ import { forwardResolvedAttrs, forwardStaticAttrs } from "./attrs.js";
2
2
  const DEFAULT_BREAKPOINTS = {
3
3
  sm: 640,
4
4
  md: 768,
@@ -165,6 +165,7 @@ export const StxImageBuiltin = {
165
165
  "picture"
166
166
  ]);
167
167
  imgAttrs.push(...forwardStaticAttrs(props, consumedStatic));
168
+ imgAttrs.push(...forwardResolvedAttrs(props, consumedStatic));
168
169
  const imgTag = `<img ${imgAttrs.join(" ")} />`;
169
170
  let preloadTag = "";
170
171
  if (preload) {
@@ -1,4 +1,4 @@
1
- import { forwardStaticAttrs } from "./attrs.js";
1
+ import { forwardResolvedAttrs, forwardStaticAttrs } from "./attrs.js";
2
2
  function escapeAttr(value) {
3
3
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4
4
  }
@@ -29,6 +29,7 @@ export const StxLinkBuiltin = {
29
29
  attrs.push("data-stx-prefetch");
30
30
  const consumedStatic = new Set(["to", "class", "className", "activeClass", "exactActiveClass", "prefetch"]);
31
31
  attrs.push(...forwardStaticAttrs(props, consumedStatic));
32
+ attrs.push(...forwardResolvedAttrs(props, consumedStatic));
32
33
  return `<a ${attrs.join(" ")}>${slotContent}</a>`;
33
34
  }
34
35
  };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Read a flag that may legitimately appear more than once.
3
+ *
4
+ * The CLI parser collapses repeats to the LAST value — `--lib a --lib b` arrives
5
+ * as the string `"b"`, and the same is true of `<path...>` and `[path...]`, so
6
+ * there is no option spec that expresses "repeatable". The typecheck command
7
+ * documented `--lib` as repeatable and had an `Array.isArray(options.lib)`
8
+ * branch that could therefore never run: a project splitting its ambient
9
+ * declarations across two files silently got whichever one it listed last
10
+ * (stacksjs/stx#1925, stacksjs/stx#1926).
11
+ *
12
+ * The failure points the wrong way, which is what made it expensive. Listing
13
+ * every declaration file — the obvious move, and what "repeatable" invites —
14
+ * lands on the UNHELPED baseline, because the one file that was helping got
15
+ * replaced by one that does not cover those names. That reads as "the flag does
16
+ * nothing" rather than "the flag is dropping all but one of my files", and it
17
+ * changes which errors are reported rather than merely how many: names that
18
+ * would resolve come back as `Cannot find name`, hiding the real diagnostics
19
+ * underneath them.
20
+ *
21
+ * So the values are read from argv, where all of them still are.
22
+ *
23
+ * @module cli-flags
24
+ */
25
+ /**
26
+ * Every value given for `flag`, in the order written.
27
+ *
28
+ * Accepts both spellings — `--lib a` and `--lib=a` — because a user who has
29
+ * been told a flag repeats will reasonably try either.
30
+ *
31
+ * Values are returned verbatim; resolving them against a directory is the
32
+ * caller's business, since it is the caller that knows what they name.
33
+ */
34
+ export declare function collectRepeatedFlag(argv: readonly string[], flag: string): string[];
@@ -0,0 +1,20 @@
1
+ export function collectRepeatedFlag(argv, flag) {
2
+ const values = [], inline = `${flag}=`;
3
+ for (let i = 0;i < argv.length; i++) {
4
+ const arg = argv[i];
5
+ if (arg.startsWith(inline)) {
6
+ const value = arg.slice(inline.length);
7
+ if (value)
8
+ values.push(value);
9
+ continue;
10
+ }
11
+ if (arg === flag) {
12
+ const next = argv[i + 1];
13
+ if (next !== void 0 && !next.startsWith("-")) {
14
+ values.push(next);
15
+ i++;
16
+ }
17
+ }
18
+ }
19
+ return values;
20
+ }