@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/reactive.js CHANGED
@@ -585,8 +585,8 @@ function finalizeTemplate(template, scopes) {
585
585
  });
586
586
  output = output.replace(/\s*x-data\s*=\s*"([^"]*)"/g, ' data-stx-xdata="$1"');
587
587
  output = output.replace(/\s*x-data\s*=\s*'([^']*)'/g, " data-stx-xdata='$1'");
588
- output = output.replace(/\s*x-init\s*=\s*"[^"]*"/g, "");
589
- output = output.replace(/\s*x-init\s*=\s*'[^']*'/g, "");
588
+ output = output.replace(/\s*x-init\s*=\s*"([^"]*)"/g, ' data-stx-xinit="$1"');
589
+ output = output.replace(/\s*x-init\s*=\s*'([^']*)'/g, " data-stx-xinit='$1'");
590
590
  return output;
591
591
  }
592
592
  export function hasReactiveDirectives(template) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Resolve whatever a user typed — a directory, a file, or a glob — to `.stx` files.
3
+ *
4
+ * The CLI commands disagreed about which of those they took, and each was silent
5
+ * about the shape it did not accept. `stx a11y` joined its argument into a scan
6
+ * pattern (`path.join(arg, '**' + '/*.stx')`), so a FILE path became
7
+ * `file.stx/**` + `/*.stx` and matched nothing, and a glob became
8
+ * `'x/**' + '/*.stx/**' + '/*.stx'` and also matched nothing. Both then took the
9
+ * empty branch, which printed
10
+ *
11
+ * ✓ No accessibility issues found!
12
+ *
13
+ * and exited 0 — on a file with three SERIOUS findings. Meanwhile `typecheck`
14
+ * globbed and, handed a directory, said it matched nothing. Both halves
15
+ * inverted, and inverted silently in the direction that costs you: the natural
16
+ * way to add a checker to CI is to copy the line that already works, so
17
+ *
18
+ * - run: stx a11y 'resources/**' + '/*.stx'
19
+ *
20
+ * passes on every commit for the life of the project, indistinguishable from a
21
+ * project with no accessibility problems (stacksjs/stx#1918).
22
+ *
23
+ * So there is one resolver and every command uses it. The separate half of that
24
+ * fix lives at the call sites and matters more: **an empty match set is not a
25
+ * pass.** A run that read no files has verified nothing, and has to say so.
26
+ *
27
+ * @module resolve-stx-targets
28
+ */
29
+ /** Is this argument a glob pattern rather than a plain path? */
30
+ export declare function looksLikeGlob(target: string): boolean;
31
+ export interface ResolveStxTargetsOptions {
32
+ /** Directory to resolve relative targets against. Defaults to the cwd. */
33
+ cwd?: string;
34
+ /** Recurse into subdirectories when the target is a directory. Default true. */
35
+ recursive?: boolean;
36
+ /** Substrings that exclude a matched path. */
37
+ ignorePaths?: readonly string[];
38
+ }
39
+ /**
40
+ * Expand targets to absolute `.stx` file paths, de-duplicated and sorted.
41
+ *
42
+ * A named FILE is returned whether or not it ends in `.stx`: naming one is an
43
+ * explicit request, and silently dropping it is the failure this module exists
44
+ * to remove. Directories and globs are filtered to `.stx`.
45
+ *
46
+ * `node_modules` and `dist` are always excluded — a glob like `**` + `/*.stx`
47
+ * otherwise sweeps up every template in every installed package.
48
+ */
49
+ export declare function resolveStxTargets(targets: readonly string[], options?: ResolveStxTargetsOptions): Promise<string[]>;
@@ -0,0 +1,38 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ const GLOB_MAGIC = /[*?[\]{}]/;
4
+ export function looksLikeGlob(target) {
5
+ return GLOB_MAGIC.test(target);
6
+ }
7
+ export async function resolveStxTargets(targets, options = {}) {
8
+ const cwd = options.cwd ?? process.cwd(), recursive = options.recursive !== !1, ignorePaths = options.ignorePaths ?? [], Glob = globalThis.Bun.Glob, found = new Set, accept = (file) => {
9
+ if (file.includes(`${path.sep}node_modules${path.sep}`) || file.includes(`${path.sep}dist${path.sep}`))
10
+ return;
11
+ if (ignorePaths.some((ignore) => ignore && file.includes(path.normalize(ignore))))
12
+ return;
13
+ found.add(file);
14
+ }, scan = async (pattern) => {
15
+ try {
16
+ for await (const file of new Glob(pattern).scan({ cwd, absolute: !0 }))
17
+ accept(file);
18
+ } catch {}
19
+ };
20
+ for (const target of targets) {
21
+ if (!target)
22
+ continue;
23
+ if (looksLikeGlob(target)) {
24
+ await scan(target);
25
+ continue;
26
+ }
27
+ const absolute = path.resolve(cwd, target);
28
+ if (!existsSync(absolute))
29
+ continue;
30
+ if (statSync(absolute).isDirectory()) {
31
+ for await (const file of new Glob(recursive ? "**/*.stx" : "*.stx").scan({ cwd: absolute, absolute: !0 }))
32
+ accept(file);
33
+ continue;
34
+ }
35
+ accept(absolute);
36
+ }
37
+ return [...found].sort();
38
+ }
@@ -168,6 +168,30 @@ export declare function templateHasReactiveContext(template: string): boolean;
168
168
  * signal-processing.ts would close a cycle through client-script.ts.
169
169
  */
170
170
  export declare function templateNeedsRuntime(template: string): boolean;
171
+ /**
172
+ * Colon attributes that are DIRECTIVES, not attribute bindings.
173
+ *
174
+ * `:href` and `:disabled` name a real HTML attribute and can be resolved to one.
175
+ * `:if`, `:for` and `:text` do not — they are instructions to the conditional
176
+ * processor, the loop runtime and the text binder, and the element carries no
177
+ * attribute of that name at any point.
178
+ *
179
+ * The distinction had never been written down, so `processServerBindings`
180
+ * evaluated every `:name="expr"` alike. It went unnoticed because its tag regex
181
+ * required each attribute chunk to start with whitespace and be a single
182
+ * character, so it never matched a real tag with a real attribute — the pass was
183
+ * effectively dead. Fixing that regex (0.2.173) switched it on for everything at
184
+ * once, and `<p :if="error">` started rendering as `<p if="Bad creds">`: the
185
+ * attribute leaked into the markup AND the conditional stopped applying, so a
186
+ * falsy condition no longer removed the element. Same for `:show`, `:text`,
187
+ * `:html`, `:model` and `:key`.
188
+ *
189
+ * `:class` and `:style` are deliberately absent: they DO resolve to real
190
+ * attributes, and `processServerBindings` has specific handling for them.
191
+ */
192
+ export declare const RUNTIME_OWNED_COLON_ATTRS: readonly string[];
193
+ /** Is this colon attribute a directive rather than something to resolve? */
194
+ export declare function isRuntimeOwnedColonAttr(name: string): boolean;
171
195
  /**
172
196
  * `x-` attributes the signals runtime consumes itself.
173
197
  *
@@ -143,6 +143,22 @@ export function templateNeedsRuntime(template) {
143
143
  return !1;
144
144
  return HAS_EVENT_HANDLER.test(blankInertHtmlRegions(template)) || templateHasReactiveContext(template);
145
145
  }
146
+ export const RUNTIME_OWNED_COLON_ATTRS = [
147
+ "if",
148
+ "else-if",
149
+ "elseif",
150
+ "else",
151
+ "for",
152
+ "show",
153
+ "key",
154
+ "text",
155
+ "html",
156
+ "model",
157
+ "ref"
158
+ ];
159
+ export function isRuntimeOwnedColonAttr(name) {
160
+ return RUNTIME_OWNED_COLON_ATTRS.includes(name.toLowerCase());
161
+ }
146
162
  export const RUNTIME_HANDLED_X_ATTRS = [
147
163
  "x-text",
148
164
  "x-html",
@@ -72,6 +72,42 @@ export declare function outputNeedsSignalsRuntime(html: string): boolean;
72
72
  * The runtime provides client-side reactivity.
73
73
  */
74
74
  export declare function injectSignalsRuntime(template: string, options: StxOptions): Promise<string>;
75
+ /**
76
+ * Whether the finished page has an element carrying `x-tooltip`.
77
+ *
78
+ * Asked of the OUTPUT rather than the source, for the reason
79
+ * `outputNeedsSignalsRuntime` is: both the attribute and every rewrite that
80
+ * could have produced it have happened by then, so no processing order can hide
81
+ * the answer.
82
+ *
83
+ * Anchored on `<` … so the name has to sit inside a tag: documentation prose
84
+ * that merely names the attribute does not pull a runtime onto the page. The
85
+ * attribute value is allowed to contain `>` — a tooltip reading `a > b` is
86
+ * ordinary — so the scan walks to the quote rather than to the first `>`.
87
+ */
88
+ export declare function outputNeedsTooltipRuntime(html: string): boolean;
89
+ /**
90
+ * Ship the `x-tooltip` runtime on any page that uses the attribute.
91
+ *
92
+ * `getTooltipRuntime()` was written, exported from `builtins/index.ts`, and
93
+ * called by nothing — two definitions and zero call sites, in stx and in
94
+ * `bun-plugin-stx`. `registerBuiltins()` registers ten builtins and tooltip is
95
+ * not one of them, so the attribute reached the DOM and nothing ever acted on
96
+ * it. Hovering showed the native `title` tooltip, which would have shown anyway
97
+ * (stacksjs/stx#1922).
98
+ *
99
+ * The dead export was not merely unused: the codemod's largest and only
100
+ * auto-fixable rule rewrites `title=` into `x-tooltip=`, so every consuming
101
+ * codebase was being pointed at an attribute that had never worked. It read as
102
+ * an adoption problem — `codemod.d.ts` records `x-tooltip` as "delivered and
103
+ * used zero times" — when the cause was that there was nothing to adopt.
104
+ *
105
+ * Not a registered builtin, because it is not a component: it is one delegated
106
+ * listener pair on `document`, so it costs nothing when no element matches and
107
+ * covers elements added later without per-element wiring. Injected only when the
108
+ * attribute is present, and before the CSP pass so a nonce still reaches it.
109
+ */
110
+ export declare function injectTooltipRuntime(html: string): string;
75
111
  /**
76
112
  * Inject the SPA router script into the template.
77
113
  * The router is provided by the canonical router in packages/router/src/client.ts.
@@ -1,6 +1,7 @@
1
1
  import { getOwnedRouteMatchers } from "./owned-routes.js";
2
2
  import { toScriptJson } from "./script-json.js";
3
3
  import { findBodyOpenTag, replaceBodyOpenTag } from "./find-body-tag.js";
4
+ import { getTooltipRuntime } from "./builtins/tooltip.js";
4
5
  export function injectBrowserRuntime(template) {
5
6
  if (template.includes("window.StacksBrowser") || template.includes("StacksBrowser ="))
6
7
  return template;
@@ -109,6 +110,19 @@ export async function injectSignalsRuntime(template, options) {
109
110
  const { getCachedSignalsRuntime } = await import("./caching.js"), runtime = await getCachedSignalsRuntime(options.debug);
110
111
  return placeRuntimeTag(template, `<script data-stx-scoped data-stx-runtime>${runtime}</script>`);
111
112
  }
113
+ export function outputNeedsTooltipRuntime(html) {
114
+ return /<[a-z][^>]*?\sx-tooltip\s*=/i.test(html);
115
+ }
116
+ export function injectTooltipRuntime(html) {
117
+ if (html.includes("data-stx-tooltip-runtime") || !outputNeedsTooltipRuntime(html))
118
+ return html;
119
+ const tag = `<script data-stx-scoped data-stx-tooltip-runtime>${getTooltipRuntime()}</script>`, bodyIdx = html.lastIndexOf("</body>");
120
+ if (bodyIdx === -1)
121
+ return `${html}
122
+ ${tag}`;
123
+ return `${html.slice(0, bodyIdx)}${tag}
124
+ ${html.slice(bodyIdx)}`;
125
+ }
112
126
  export async function injectRouterScript(template, options) {
113
127
  if (!template.includes("</body>"))
114
128
  return template;
@@ -1,3 +1,4 @@
1
+ import { isRuntimeOwnedColonAttr } from "./runtime-globals.js";
1
2
  const BOOLEAN_ATTRS = new Set([
2
3
  "disabled",
3
4
  "checked",
@@ -96,6 +97,8 @@ export function processServerBindings(template, context) {
96
97
  let attrMatch, newAttrs = attrs, hadReplacement = !1;
97
98
  while ((attrMatch = bindingRegex.exec(attrs)) !== null) {
98
99
  const attrName = attrMatch[1], expression = attrMatch[2];
100
+ if (isRuntimeOwnedColonAttr(attrName))
101
+ continue;
99
102
  if (expression.includes("()") && !expression.includes(".get(") || expression.includes("$store") || expression.includes("$ref"))
100
103
  continue;
101
104
  try {
@@ -90,6 +90,30 @@ function extractClientSignalNames(template) {
90
90
  }
91
91
  return names;
92
92
  }
93
+ function collectClientLoopScopes(template) {
94
+ if (!/[:@]for\s*=/.test(template) && !/\bx-for\s*=/.test(template))
95
+ return [];
96
+ const openingTag = new RegExp(`<([a-zA-Z][\\w-]*)\\b(${TAG_ATTR_RUN})>`, "iy"), loopAttr = /(?:^|\s)(?::for|@for|x-for)\s*=\s*(?:"([^"]*)"|'([^']*)')/i, starts = scanAtElementPosition(template, (html, index) => {
97
+ openingTag.lastIndex = index;
98
+ const match = openingTag.exec(html);
99
+ if (!match || match.index !== index || !loopAttr.test(match[2]))
100
+ return -1;
101
+ return openingTag.lastIndex;
102
+ }), scopes = [];
103
+ for (const start of starts) {
104
+ openingTag.lastIndex = 0;
105
+ const match = openingTag.exec(start.token);
106
+ if (!match)
107
+ continue;
108
+ const attrMatch = match[2].match(loopAttr), parsed = parseLoopExpression(attrMatch?.[1] ?? attrMatch?.[2] ?? ""), vars = [parsed.itemVar, parsed.indexVar].filter(Boolean);
109
+ if (vars.length === 0)
110
+ continue;
111
+ const tag = match[1], end = /\/\s*>$/.test(start.token) || VOID_ELEMENT_TAGS.has(tag.toLowerCase()) ? start.end : findElementEnd(template, start.start, tag, start.end);
112
+ if (end !== -1)
113
+ scopes.push({ start: start.start, end, vars: new Set(vars) });
114
+ }
115
+ return scopes;
116
+ }
93
117
  const JS_NON_VARS = new Set([
94
118
  "true",
95
119
  "false",
@@ -132,7 +156,7 @@ const JS_NON_VARS = new Set([
132
156
  function stripExprStrings(expr) {
133
157
  return expr.replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
134
158
  }
135
- function conditionIsClientReactive(condition, signalNames, context) {
159
+ function conditionIsClientReactive(condition, signalNames, context, clientLoopVars) {
136
160
  if (!condition)
137
161
  return !1;
138
162
  const expr = stripExprStrings(condition);
@@ -145,7 +169,7 @@ function conditionIsClientReactive(condition, signalNames, context) {
145
169
  continue;
146
170
  if (JS_NON_VARS.has(id))
147
171
  continue;
148
- if (signalNames.has(id))
172
+ if (signalNames.has(id) || clientLoopVars?.has(id))
149
173
  refsSignal = !0;
150
174
  else if (context && id in context)
151
175
  refsServerVar = !0;
@@ -186,7 +210,14 @@ export function convertSignalDirectivesToAttributes(template, context) {
186
210
  const signalNames = extractClientSignalNames(template);
187
211
  function convert(input) {
188
212
  let output = input;
189
- const ifDirectiveStart = /@if\s*\(/g;
213
+ const clientLoopScopes = collectClientLoopScopes(input), loopVarsAt = (position) => {
214
+ const vars = new Set;
215
+ for (const scope of clientLoopScopes)
216
+ if (position > scope.start && position < scope.end)
217
+ for (const name of scope.vars)
218
+ vars.add(name);
219
+ return vars;
220
+ }, ifDirectiveStart = /@if\s*\(/g;
190
221
  let match;
191
222
  const replacements = [];
192
223
  while ((match = ifDirectiveStart.exec(output)) !== null) {
@@ -224,8 +255,8 @@ export function convertSignalDirectivesToAttributes(template, context) {
224
255
  const parsed = findIfBlocks(output.substring(startIdx, endIdx));
225
256
  if (parsed.length === 0)
226
257
  continue;
227
- const valueBranches = parsed[0].branches.filter((b) => b.type !== "else");
228
- if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context))))
258
+ const valueBranches = parsed[0].branches.filter((b) => b.type !== "else"), clientLoopVars = loopVarsAt(startIdx);
259
+ if (!(valueBranches.length > 0 && valueBranches.every((b) => conditionIsClientReactive(b.condition, signalNames, context, clientLoopVars))))
229
260
  continue;
230
261
  replacement = parsed[0].branches.map((branch) => {
231
262
  const branchContent = convert(branch.content);
@@ -237,7 +268,7 @@ export function convertSignalDirectivesToAttributes(template, context) {
237
268
  }).join(`
238
269
  `);
239
270
  } else {
240
- if (!conditionIsClientReactive(condition, signalNames, context))
271
+ if (!conditionIsClientReactive(condition, signalNames, context, loopVarsAt(startIdx)))
241
272
  continue;
242
273
  replacement = branchToAttrElement(convert(content), "@if", condition);
243
274
  }
@@ -801,7 +832,7 @@ export function extractExports(setupContent) {
801
832
  }, checkDeclaration = () => {
802
833
  if (depth !== 0)
803
834
  return;
804
- const declMatch = code.slice(i).match(/^(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/);
835
+ const declMatch = code.slice(i).match(/^(const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:[!:;,\n]|=(?!=)|$)/);
805
836
  if (declMatch) {
806
837
  const varName = declMatch[2];
807
838
  if (!seen.has(varName)) {