@stacksjs/stx 0.2.170 → 0.2.172

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 (44) hide show
  1. package/dist/cli.js +1263 -1168
  2. package/dist/client-script-bundler.js +2 -1
  3. package/dist/client-script.d.ts +1 -12
  4. package/dist/client-script.js +2 -149
  5. package/dist/codemod.d.ts +1 -24
  6. package/dist/codemod.js +61 -9
  7. package/dist/compression.d.ts +11 -4
  8. package/dist/compression.js +52 -13
  9. package/dist/craft.js +489 -415
  10. package/dist/expressions.d.ts +0 -21
  11. package/dist/expressions.js +12 -1
  12. package/dist/find-body-tag.js +29 -17
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.js +2 -0
  15. package/dist/page-action.d.ts +83 -0
  16. package/dist/page-action.js +64 -0
  17. package/dist/production-builder.js +1 -0
  18. package/dist/production-server.js +10 -3
  19. package/dist/pwa.js +398 -324
  20. package/dist/reactive-tracking.d.ts +81 -0
  21. package/dist/reactive-tracking.js +49 -0
  22. package/dist/reactivity.js +29 -23
  23. package/dist/render.js +5 -18
  24. package/dist/runtime-injection.js +25 -39
  25. package/dist/script-validation.d.ts +0 -7
  26. package/dist/script-validation.js +6 -4
  27. package/dist/signals-api.d.ts +18 -0
  28. package/dist/signals-api.js +26 -41
  29. package/dist/signals.js +93 -15
  30. package/dist/ssg.d.ts +8 -0
  31. package/dist/ssg.js +515 -441
  32. package/dist/state-management.js +2 -2
  33. package/dist/strip-literals.d.ts +40 -0
  34. package/dist/strip-literals.js +149 -0
  35. package/dist/stx-virtual-ts.d.ts +103 -1
  36. package/dist/stx-virtual-ts.js +124 -7
  37. package/dist/template-compiler.js +4 -2
  38. package/dist/template-hydrator.d.ts +1 -1
  39. package/dist/template-hydrator.js +13 -1
  40. package/dist/typecheck.d.ts +2 -14
  41. package/dist/typecheck.js +133 -11
  42. package/dist/types/config-types.d.ts +10 -3
  43. package/dist/variable-extractor.js +1 -1
  44. package/package.json +5 -5
@@ -231,8 +231,8 @@ export function createAction(store, name, handler) {
231
231
  return async (...params) => {
232
232
  const currentState = store.get(), newState = await handler(currentState, ...params);
233
233
  store.set(newState);
234
- if (typeof window < "u" && window.__STX_DEVTOOLS__)
235
- window.__STX_DEVTOOLS__.onAction(store.name || "unnamed", name, params);
234
+ if (typeof window < "u")
235
+ window.__STX_DEVTOOLS__?.onAction?.(store.name || "unnamed", name, params);
236
236
  };
237
237
  }
238
238
  export function createActions(store, actions) {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Blank out comments and string literals, keeping every other character where
3
+ * it was.
4
+ *
5
+ * A name mentioned in a comment or inside a string is not a reference to it.
6
+ * The server-to-client bridge applies this before deciding what a client block
7
+ * mentions, and `stx typecheck` has to apply exactly the same rule before
8
+ * reporting what still crosses implicitly — a second copy that disagreed would
9
+ * make the warning and the runtime describe different sets (#1868 ask 4).
10
+ *
11
+ * Lives in its own module for that reason. It was defined inside
12
+ * `client-script.ts`, and the editor-facing `stx-virtual-ts.ts` cannot import
13
+ * that without pulling the bundler in behind it; the alternative was a
14
+ * second implementation, which is the failure this repo keeps repeating.
15
+ *
16
+ * Positions are preserved: every blanked character becomes a space and every
17
+ * newline stays a newline, so an offset into the result is an offset into the
18
+ * source.
19
+ *
20
+ * @module strip-literals
21
+ */
22
+ /**
23
+ * Index of the `}` closing an interpolation whose body starts at `start`.
24
+ *
25
+ * Literals and comments are skipped whole, so a brace inside them cannot be
26
+ * mistaken for structure — `` `${ x ? "}" : y }` `` closes at the right place.
27
+ */
28
+ export declare function findInterpolationEnd(code: string, start: number): number;
29
+ /**
30
+ * Blank out comments and string literals, preserving length and line structure
31
+ * so offsets still line up, so that identifier detection only ever sees code.
32
+ *
33
+ * Template-literal INTERPOLATIONS are kept, because `${...}` is code, not text.
34
+ * Blanking them hid every identifier that appears only inside one — a helper
35
+ * called as `${formatDate(d)}` looked unused to the auto-import scanner, and a
36
+ * server value read as `` `/api/users/${userId}` `` looked unreferenced to the
37
+ * data bridge, which would have withheld it and left the client throwing on an
38
+ * undefined name.
39
+ */
40
+ export declare function stripCommentsAndLiterals(code: string): string;
@@ -0,0 +1,149 @@
1
+ function skipQuoted(code, start) {
2
+ const quote = code[start];
3
+ let i = start + 1;
4
+ while (i < code.length) {
5
+ if (code[i] === "\\") {
6
+ i += 2;
7
+ continue;
8
+ }
9
+ if (code[i] === quote)
10
+ return i + 1;
11
+ i++;
12
+ }
13
+ return code.length;
14
+ }
15
+ function skipTemplate(code, start) {
16
+ let i = start + 1;
17
+ while (i < code.length) {
18
+ if (code[i] === "\\") {
19
+ i += 2;
20
+ continue;
21
+ }
22
+ if (code[i] === "`")
23
+ return i + 1;
24
+ if (code[i] === "$" && code[i + 1] === "{") {
25
+ i = findInterpolationEnd(code, i + 2);
26
+ if (i < code.length)
27
+ i++;
28
+ continue;
29
+ }
30
+ i++;
31
+ }
32
+ return code.length;
33
+ }
34
+ export function findInterpolationEnd(code, start) {
35
+ let depth = 1, i = start;
36
+ while (i < code.length) {
37
+ const ch = code[i];
38
+ if (ch === "\\") {
39
+ i += 2;
40
+ continue;
41
+ }
42
+ if (ch === '"' || ch === "'") {
43
+ i = skipQuoted(code, i);
44
+ continue;
45
+ }
46
+ if (ch === "`") {
47
+ i = skipTemplate(code, i);
48
+ continue;
49
+ }
50
+ if (ch === "/" && code[i + 1] === "/") {
51
+ while (i < code.length && code[i] !== `
52
+ `)
53
+ i++;
54
+ continue;
55
+ }
56
+ if (ch === "/" && code[i + 1] === "*") {
57
+ i += 2;
58
+ while (i < code.length && code.slice(i, i + 2) !== "*/")
59
+ i++;
60
+ i += 2;
61
+ continue;
62
+ }
63
+ if (ch === "{") {
64
+ depth++;
65
+ i++;
66
+ continue;
67
+ }
68
+ if (ch === "}") {
69
+ depth--;
70
+ if (depth === 0)
71
+ return i;
72
+ i++;
73
+ continue;
74
+ }
75
+ i++;
76
+ }
77
+ return code.length;
78
+ }
79
+ export function stripCommentsAndLiterals(code) {
80
+ let out = "", i = 0;
81
+ while (i < code.length) {
82
+ const two = code.slice(i, i + 2);
83
+ if (two === "//") {
84
+ while (i < code.length && code[i] !== `
85
+ `)
86
+ out += " ", i++;
87
+ continue;
88
+ }
89
+ if (two === "/*") {
90
+ while (i < code.length && code.slice(i, i + 2) !== "*/")
91
+ out += code[i] === `
92
+ ` ? `
93
+ ` : " ", i++;
94
+ out += " ";
95
+ i += 2;
96
+ continue;
97
+ }
98
+ const ch = code[i];
99
+ if (ch === '"' || ch === "'") {
100
+ const quote = ch;
101
+ out += " ";
102
+ i++;
103
+ while (i < code.length && code[i] !== quote) {
104
+ if (code[i] === "\\") {
105
+ out += " ";
106
+ i += 2;
107
+ continue;
108
+ }
109
+ out += code[i] === `
110
+ ` ? `
111
+ ` : " ";
112
+ i++;
113
+ }
114
+ out += " ";
115
+ i++;
116
+ continue;
117
+ }
118
+ if (ch === "`") {
119
+ out += " ";
120
+ i++;
121
+ while (i < code.length && code[i] !== "`") {
122
+ if (code[i] === "\\") {
123
+ out += " ";
124
+ i += 2;
125
+ continue;
126
+ }
127
+ if (code[i] === "$" && code[i + 1] === "{") {
128
+ const bodyStart = i + 2, bodyEnd = findInterpolationEnd(code, bodyStart);
129
+ out += " ";
130
+ out += stripCommentsAndLiterals(code.slice(bodyStart, bodyEnd));
131
+ if (bodyEnd < code.length)
132
+ out += " ";
133
+ i = bodyEnd + 1;
134
+ continue;
135
+ }
136
+ out += code[i] === `
137
+ ` ? `
138
+ ` : " ";
139
+ i++;
140
+ }
141
+ out += " ";
142
+ i++;
143
+ continue;
144
+ }
145
+ out += ch;
146
+ i++;
147
+ }
148
+ return out;
149
+ }
@@ -24,6 +24,27 @@ export declare function maskNonTemplateRegions(source: string): string;
24
24
  */
25
25
  export declare function stripFilterChain(expression: string): string;
26
26
  export declare function substituteInterpolations(value: string): string;
27
+ /**
28
+ * The same substitution, but the result occupies the same space as the source.
29
+ *
30
+ * A `<script client>` block may take a server value through an interpolation —
31
+ * `const duration = {{ duration }}` — and 48 of the framework's own 95
32
+ * components do exactly that. As TypeScript that is a syntax error, so
33
+ * `stx typecheck` reported 369 of them across the component library: a parse
34
+ * failure, which suppresses every real diagnostic in the same file. The gate
35
+ * was not strict there, it was blind.
36
+ *
37
+ * Position-preserving because these are script blocks, not attribute values.
38
+ * A diagnostic on the line after an interpolation has to keep pointing at the
39
+ * code the author wrote, so the replacement is padded to the original width and
40
+ * carries the original newline count. For a single-line interpolation — all but
41
+ * a handful — every column after it is preserved exactly.
42
+ *
43
+ * `0` rather than a named placeholder because it fits in `{{a}}`, the shortest
44
+ * interpolation anyone writes; a longer identifier could not be padded DOWN to
45
+ * fit and would shift the line.
46
+ */
47
+ export declare function substituteInterpolationsInPlace(code: string): string;
27
48
  /**
28
49
  * Every checkable expression in the markup, with the position it occupies.
29
50
  *
@@ -42,6 +63,83 @@ export declare function collectBlockDeclarations(code: string): string[];
42
63
  /** Ambient declarations for the globals the stx runtime injects. */
43
64
  export declare function runtimeGlobalDeclarations(): string;
44
65
  export declare function serverContextDeclarations(): string;
66
+ /**
67
+ * The names a `defineClientPayload({ … })` call publishes, or `null` when the
68
+ * page does not declare one.
69
+ *
70
+ * Only the keys matter. The runtime captures values, and `extractBridgeData`
71
+ * publishes the declared set in full and nothing else, so the key set IS the
72
+ * contract between the two blocks (#1868).
73
+ *
74
+ * `null` and `[]` mean different things: `null` is a page that never declared,
75
+ * and still gets the old name-scraping bridge; `[]` is a page that declared an
76
+ * empty payload and therefore publishes nothing.
77
+ */
78
+ export declare function extractClientPayloadNames(serverCode: string): string[] | null;
79
+ /** Top-level `import` statements, and everything else, separated. */
80
+ export declare function splitTopLevelImports(code: string): { imports: string, body: string };
81
+ /**
82
+ * Declarations that give a client block the server values it is actually given.
83
+ *
84
+ * A `<script client>` block is checked in its own buffer, so a name reaching it
85
+ * through the server-to-client bridge was simply "Cannot find name" — which
86
+ * means `stx typecheck` reported an error for every page that used the bridge
87
+ * at all. A checker that invents errors gets muted, and a muted gate catches
88
+ * nothing.
89
+ *
90
+ * The types are inferred rather than guessed. The server block is re-emitted
91
+ * inside a wrapper function that returns exactly the declared payload, and each
92
+ * published name is then declared as a lookup into that function's return type,
93
+ * so `const liveNow = await countLive()` reaches the client block as whatever
94
+ * `countLive` returns — not `any`, and not a hand-written annotation that can
95
+ * drift from the code above it (#1868 ask 2).
96
+ *
97
+ * Everything is emitted with `var`, `function` and `type`, all of which hoist,
98
+ * so this appends to the buffer and no line number moves.
99
+ *
100
+ * A page that declares nothing still gets its server bindings, as `any`. That
101
+ * is exactly what the scraping bridge does — publish any server name the client
102
+ * source mentions — and declaring `any` is the honest description of a value
103
+ * whose delivery depends on textual reference. Omitting them instead would make
104
+ * `stx typecheck` report "Cannot find name" on every page that uses the bridge
105
+ * at all, and a checker that invents errors gets muted.
106
+ *
107
+ * So the incentive runs the right way: declare a payload and the names are
108
+ * checked against the server block, or do not and they are `any`.
109
+ */
110
+ export declare function clientPayloadDeclarations(serverCode: string): string;
111
+ /**
112
+ * Declarations at the TOP LEVEL of a block, ignoring anything nested.
113
+ *
114
+ * `collectBlockDeclarations` matches `^\s*const …` with leading whitespace
115
+ * allowed, so it also returns bindings local to a function. For most callers
116
+ * that is harmless. For the bridge warning it is not: a `const state` inside a
117
+ * helper never crosses to the client, and naming it told the author to declare
118
+ * a payload entry that would do nothing (stacksjs/stx#1908).
119
+ *
120
+ * Nested braces are blanked first, so only depth-zero declarations survive to
121
+ * be matched by the shared patterns.
122
+ */
123
+ export declare function collectTopLevelDeclarations(code: string): string[];
124
+ /**
125
+ * Server bindings a client block reaches for without a declared payload.
126
+ *
127
+ * The scraping bridge publishes a server binding when its name appears as a
128
+ * free identifier in the client source, so these DO arrive at runtime — they
129
+ * are just untyped, and their delivery depends on a textual coincidence rather
130
+ * than on anything the page states. Reporting them is #1868 ask 4, and it is
131
+ * only worth doing now that ask 2 gives them somewhere to go: warning people
132
+ * off the implicit path before the typed replacement existed would have been
133
+ * noise.
134
+ *
135
+ * A warning, not an error. These pages work; the fix is mechanical and the
136
+ * reward is types rather than correctness, so a gate that fails CI over it
137
+ * would be punishing an app for predating the feature.
138
+ *
139
+ * Empty once the page declares a payload, and empty when the client block does
140
+ * not actually mention any server name.
141
+ */
142
+ export declare function scrapedBridgeNames(serverCode: string, clientCode: string): string[];
45
143
  /**
46
144
  * Names declared by both a server block and a client/plain block.
47
145
  *
@@ -103,7 +201,11 @@ export declare const UNWRAP_HELPER: '__StxTemplateValue';
103
201
  * Mirrors what `serve.ts` puts in scope. Declaring these is what lets the
104
202
  * checker report a genuine typo instead of suppressing every unresolved name.
105
203
  */
106
- export declare const STX_SERVER_CONTEXT: readonly ['params', 'query', 'cookies', 'host', 'ip', 'setResponseStatus', 'setResponseHeader', 'notFound', 'request', 'url', 'definePageMeta'];
204
+ export declare const STX_SERVER_CONTEXT: readonly ['params', 'query', 'cookies', 'host', 'ip', 'setResponseStatus', 'setResponseHeader', 'notFound', 'request', 'url', 'definePageMeta', unknown, unknown, 'defineStore'];
205
+ /** Marker comment so a generated preamble is recognisable in a dumped buffer. */
206
+ export declare const PAYLOAD_COMMENT: 'stx: types for the declared client payload (#1868)';
207
+ /** The same, for a page still relying on the name-scraping bridge. */
208
+ export declare const SCRAPED_COMMENT: 'stx: untyped bridge names — declare a defineClientPayload to check these (#1868)';
107
209
  /**
108
210
  * Names the template engine puts in scope for every page.
109
211
  *
@@ -1,17 +1,22 @@
1
+ import { stripCommentsAndLiterals } from "./strip-literals.js";
1
2
  import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
2
3
  const SCRIPT_RE = /<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi;
4
+ function blankCommentContents(source) {
5
+ const blank = (match) => match.replace(/[^\n]/g, " ");
6
+ return source.replace(/<!--[\s\S]*?-->/g, blank).replace(/\{\{--[\s\S]*?--\}\}/g, blank);
7
+ }
3
8
  export function extractScriptBlocks(source) {
4
- const blocks = [];
9
+ const blocks = [], scannable = blankCommentContents(source);
5
10
  SCRIPT_RE.lastIndex = 0;
6
11
  let match;
7
- while ((match = SCRIPT_RE.exec(source)) !== null) {
8
- const attrs = (match[1] || "").trim(), body = match[2] ?? "";
12
+ while ((match = SCRIPT_RE.exec(scannable)) !== null) {
13
+ const attrs = (match[1] || "").trim(), openLength = (match[0].match(/^<script(?:\s[^>]*)?>/i)?.[0] ?? "<script>").length, closeLength = (match[0].match(/<\/script\s*>$/i)?.[0] ?? "</script>").length, bodyStart = match.index + openLength, bodyEnd = match.index + match[0].length - closeLength, body = source.slice(bodyStart, bodyEnd);
9
14
  if (/\bsrc\s*=/.test(attrs))
10
15
  continue;
11
16
  const typeMatch = attrs.match(/\btype\s*=\s*["']([^"']+)["']/i);
12
17
  if (typeMatch && !/^(?:module|text\/(?:javascript|typescript))$/i.test(typeMatch[1]))
13
18
  continue;
14
- const kind = /\bserver\b/.test(attrs) ? "server" : /\bclient\b/.test(attrs) ? "client" : "plain", openTag = match[0].match(/^<script[^>]*>/i)?.[0] ?? "<script>", startLine = lineAt(source, match.index + openTag.length);
19
+ const kind = /\bserver\b/.test(attrs) ? "server" : /\bclient\b/.test(attrs) ? "client" : "plain", startLine = lineAt(source, bodyStart);
15
20
  blocks.push({ kind, code: body, startLine, attrs });
16
21
  }
17
22
  return blocks;
@@ -92,6 +97,14 @@ const UNWRAP_HELPER_DECL = `type ${UNWRAP_HELPER}<T> = T extends { readonly _isS
92
97
  export function substituteInterpolations(value) {
93
98
  return value.replace(/\{\{[\s\S]*?\}\}/g, INTERPOLATION_PLACEHOLDER).replace(/\{!![\s\S]*?!!\}/g, INTERPOLATION_PLACEHOLDER);
94
99
  }
100
+ export function substituteInterpolationsInPlace(code) {
101
+ return code.replace(/\{\{[\s\S]*?\}\}|\{!![\s\S]*?!!\}/g, (match) => {
102
+ const newlines = match.split(`
103
+ `).length - 1, width = match.length - newlines;
104
+ return `0${" ".repeat(Math.max(0, width - 1))}${`
105
+ `.repeat(newlines)}`;
106
+ });
107
+ }
95
108
  const NON_EXPRESSION_ATTRS = new Set([
96
109
  "x-cloak",
97
110
  "x-ref",
@@ -99,7 +112,9 @@ const NON_EXPRESSION_ATTRS = new Set([
99
112
  "x-teleport",
100
113
  "v-slot",
101
114
  "x-hydrate",
102
- ":key"
115
+ ":key",
116
+ "x-tooltip",
117
+ "x-tooltip-position"
103
118
  ]);
104
119
  export function extractTemplateExpressions(source) {
105
120
  const masked = maskNonTemplateRegions(source), found = [], interpolation = /(@?)\{\{([\s\S]*?)\}\}|(@?)\{!!([\s\S]*?)!!\}/g;
@@ -120,7 +135,7 @@ export function extractTemplateExpressions(source) {
120
135
  }
121
136
  DIRECTIVE_ATTR_RE.lastIndex = 0;
122
137
  while ((m = DIRECTIVE_ATTR_RE.exec(masked)) !== null) {
123
- const name = m[1], value = m[3] ?? "", base = name.replace(/\..*$/, "");
138
+ const name = m[1], value = m[3] ?? "", base = name.replace(/\..*$/, "").replace(/^([^:].*?):.*$/, "$1");
124
139
  if (NON_EXPRESSION_ATTRS.has(base) || NON_EXPRESSION_ATTRS.has(name))
125
140
  continue;
126
141
  if (!value.trim())
@@ -229,12 +244,114 @@ export const STX_SERVER_CONTEXT = [
229
244
  "notFound",
230
245
  "request",
231
246
  "url",
232
- "definePageMeta"
247
+ "definePageMeta",
248
+ "defineClientPayload",
249
+ "defineStore"
233
250
  ];
234
251
  export function serverContextDeclarations() {
235
252
  return STX_SERVER_CONTEXT.map((name) => `declare var ${name}: any`).join(`
236
253
  `);
237
254
  }
255
+ export function extractClientPayloadNames(serverCode) {
256
+ const call = /\bdefineClientPayload\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
257
+ let match, found = !1;
258
+ const names = new Set, searchable = stripCommentsAndLiterals(serverCode);
259
+ while ((match = call.exec(searchable)) !== null) {
260
+ found = !0;
261
+ for (const entry of splitPayloadEntries(match[1])) {
262
+ const name = entry.split(":")[0].trim();
263
+ if (/^[A-Za-z_$][\w$]*$/.test(name))
264
+ names.add(name);
265
+ }
266
+ }
267
+ return found ? [...names] : null;
268
+ }
269
+ function splitPayloadEntries(body) {
270
+ const entries = [];
271
+ let depth = 0, current = "";
272
+ for (const char of body) {
273
+ if (char === "," && depth === 0) {
274
+ entries.push(current);
275
+ current = "";
276
+ continue;
277
+ }
278
+ if (char === "(" || char === "[" || char === "{")
279
+ depth++;
280
+ else if (char === ")" || char === "]" || char === "}")
281
+ depth--;
282
+ current += char;
283
+ }
284
+ entries.push(current);
285
+ return entries.filter((entry) => entry.trim().length > 0);
286
+ }
287
+ export function splitTopLevelImports(code) {
288
+ const importLine = /^[ \t]*import\b[\s\S]*?(?:from\s*['"][^'"]*['"]|['"][^'"]*['"])[ \t]*;?[ \t]*$/gm, imports = [], body = code.replace(importLine, (statement) => {
289
+ imports.push(statement.trim());
290
+ return statement.split(`
291
+ `).map(() => "").join(`
292
+ `);
293
+ });
294
+ return { imports: imports.join(`
295
+ `), body };
296
+ }
297
+ export function clientPayloadDeclarations(serverCode) {
298
+ const published = extractClientPayloadNames(serverCode);
299
+ if (!published) {
300
+ const scraped = collectBlockDeclarations(serverCode);
301
+ if (scraped.length === 0)
302
+ return "";
303
+ return [
304
+ "",
305
+ `// ${SCRAPED_COMMENT}`,
306
+ ...scraped.map((name) => `declare var ${name}: any`)
307
+ ].join(`
308
+ `);
309
+ }
310
+ if (published.length === 0)
311
+ return "";
312
+ const { imports, body } = splitTopLevelImports(serverCode), scopeBody = body.replace(/^[ \t]*export[ \t]+(?=(?:const|let|var|function|async|class)\b)/gm, ""), parts = [
313
+ "",
314
+ `// ${PAYLOAD_COMMENT}`
315
+ ];
316
+ if (imports)
317
+ parts.push(imports);
318
+ parts.push(`async function ${PAYLOAD_SCOPE}() {`, scopeBody, ` return { ${published.join(", ")} }`, "}", `type ${PAYLOAD_TYPE} = Awaited<ReturnType<typeof ${PAYLOAD_SCOPE}>>`, ...published.map((name) => `declare var ${name}: ${PAYLOAD_TYPE}[${JSON.stringify(name)}]`));
319
+ return parts.join(`
320
+ `);
321
+ }
322
+ export function collectTopLevelDeclarations(code) {
323
+ const chars = stripCommentsAndLiterals(code).split("");
324
+ let depth = 0;
325
+ for (let i = 0;i < chars.length; i++) {
326
+ const c = chars[i];
327
+ if (c === "{" || c === "(" || c === "[") {
328
+ depth++;
329
+ continue;
330
+ }
331
+ if (c === "}" || c === ")" || c === "]") {
332
+ depth = Math.max(0, depth - 1);
333
+ continue;
334
+ }
335
+ if (depth > 0 && c !== `
336
+ `)
337
+ chars[i] = " ";
338
+ }
339
+ return collectBlockDeclarations(chars.join(""));
340
+ }
341
+ export function scrapedBridgeNames(serverCode, clientCode) {
342
+ if (extractClientPayloadNames(serverCode) !== null)
343
+ return [];
344
+ const searchable = stripCommentsAndLiterals(clientCode), provided = new Set(STX_RUNTIME_GLOBALS);
345
+ return collectTopLevelDeclarations(serverCode).filter((name) => {
346
+ if (provided.has(name))
347
+ return !1;
348
+ if (collectBlockDeclarations(clientCode).includes(name))
349
+ return !1;
350
+ return new RegExp(`(?<![.#\\w$])${name}(?![\\w$])`).test(searchable);
351
+ });
352
+ }
353
+ export const PAYLOAD_COMMENT = "stx: types for the declared client payload (#1868)", SCRAPED_COMMENT = "stx: untyped bridge names \u2014 declare a defineClientPayload to check these (#1868)";
354
+ const PAYLOAD_SCOPE = "__stxServerScope", PAYLOAD_TYPE = "__StxClientPayload", PAYLOAD_NAMESPACE = "__StxServerBlock";
238
355
  export const STX_TEMPLATE_GLOBALS = [
239
356
  "$env",
240
357
  "$user",
@@ -5,6 +5,7 @@ import { extractVariables } from "./variable-extractor.js";
5
5
  import { createPlaceholder, resetPlaceholders, getPlaceholderRegistry } from "./placeholder.js";
6
6
  import { stripDocumentWrapper } from "./app-shell.js";
7
7
  import { createHash } from "node:crypto";
8
+ import { collectBlockDeclarations } from "./stx-virtual-ts.js";
8
9
  function createRecordingContext(baseContext = {}) {
9
10
  const accessedKeys = new Set;
10
11
  return { context: new Proxy(baseContext, {
@@ -38,9 +39,10 @@ export async function compileTemplate(filePath, route, options = {}) {
38
39
  if (/\bserver\b/.test(attrs))
39
40
  serverScriptContent.push(scriptMatch[2]);
40
41
  }
41
- const { context } = createRecordingContext({
42
+ const serverDeclared = new Set(serverScriptContent.flatMap((code) => collectBlockDeclarations(code))), { context } = createRecordingContext({
42
43
  __filename: absolutePath,
43
- __dirname: path.dirname(absolutePath)
44
+ __dirname: path.dirname(absolutePath),
45
+ __stx_serverDeclared: serverDeclared
44
46
  }), dependencies = new Set, opts = {
45
47
  ...defaultConfig,
46
48
  ...projectConfig,
@@ -20,7 +20,7 @@ export declare function hydrateTemplate(compiled: CompiledTemplate, requestConte
20
20
  * templates aren't serialized into the compiled template, so use the function
21
21
  * form in production.)
22
22
  */
23
- export declare function hydrateTemplateStream(compiled: CompiledTemplate, requestContext?: Record<string, any>): Promise<{ html: string, boundaries?: { id: string, render: () => Promise<string> }[] }>;
23
+ export declare function hydrateTemplateStream(compiled: CompiledTemplate, requestContext?: Record<string, any>): Promise<{ html: string, boundaries?: { id: string, render: () => Promise<string> }[], redirect?: string }>;
24
24
  /**
25
25
  * Hydrate a fragment (for SPA navigation).
26
26
  * Same as hydrateTemplate but returns the fragment portion.
@@ -1,11 +1,15 @@
1
1
  import { extractVariables } from "./variable-extractor.js";
2
2
  import { evaluateExpression, escapeHtml } from "./expressions.js";
3
3
  import { replacePlaceholders, hasPlaceholders } from "./placeholder.js";
4
+ import { runPageAction } from "./page-action.js";
4
5
  export async function hydrateTemplate(compiled, requestContext = {}) {
5
6
  return (await runHydration(compiled, requestContext)).html;
6
7
  }
7
8
  export async function hydrateTemplateStream(compiled, requestContext = {}) {
8
- const { html, context } = await runHydration(compiled, requestContext), { extractStreamBoundaries } = await import("./streaming.js");
9
+ const { html, context, redirect } = await runHydration(compiled, requestContext);
10
+ if (redirect)
11
+ return { html, redirect };
12
+ const { extractStreamBoundaries } = await import("./streaming.js");
9
13
  let boundaries = extractStreamBoundaries(context);
10
14
  const templates = compiled.streamTemplates;
11
15
  if (templates && Object.keys(templates).length > 0) {
@@ -34,6 +38,14 @@ async function runHydration(compiled, requestContext) {
34
38
  } catch (error) {
35
39
  console.warn(`[stx] Server script error in ${compiled.sourceFile}:`, error);
36
40
  }
41
+ const action = await runPageAction(context, {
42
+ request: requestContext.request,
43
+ method: requestContext.method,
44
+ params: requestContext.params ?? {},
45
+ cookies: requestContext.cookies ?? {}
46
+ });
47
+ if (action.redirect)
48
+ return { html: compiled.html, context, redirect: action.redirect };
37
49
  if (!hasPlaceholders(compiled.html))
38
50
  return { html: compiled.html, context };
39
51
  const values = new Map;
@@ -15,7 +15,7 @@ export type { ScriptBlock, ScriptKind } from './stx-virtual-ts';
15
15
  * variable", and those are ordinary names for server-rendered page data. It is
16
16
  * appended rather than prepended so the line numbers above it are untouched.
17
17
  */
18
- export declare function buildVirtualSource(block: ScriptBlock): string;
18
+ export declare function buildVirtualSource(block: ScriptBlock, serverCode?: string): string;
19
19
  /** Virtual path for a block, stable and traceable back to its origin. */
20
20
  export declare function virtualPathFor(filePath: string, kind: DiagnosticOrigin, index: number): string;
21
21
  /** Map a virtual path back to the `.stx` file it came from. */
@@ -29,19 +29,6 @@ export declare function sourcePathFor(virtualPath: string): string;
29
29
  * than hand tsc a path to nothing.
30
30
  */
31
31
  export declare function findRuntimeTypeDeclarations(): string | null;
32
- /**
33
- * Type-check `.stx` files.
34
- *
35
- * Runs the `tsc` BINARY over generated virtual files rather than driving the
36
- * compiler API. TypeScript 7 ships as a Go port whose npm package exposes only
37
- * `version` — no `createProgram`, no `ScriptTarget` — so an API-based checker
38
- * would work on TS 5 and break on the version this repo actually resolves.
39
- * Shelling out works across both.
40
- *
41
- * TypeScript is not a declared dependency: anyone running a type-check already
42
- * has it, and making the package depend on a compiler to serve a page would be
43
- * the wrong trade.
44
- */
45
32
  export declare function typecheckStxFiles(files: string[], options?: TypecheckOptions): Promise<TypecheckResult>;
46
33
  /** Render diagnostics the way a CLI should: file:line:col, then the message. */
47
34
  export declare function formatTypecheckDiagnostics(diagnostics: TypecheckDiagnostic[]): string;
@@ -64,6 +51,7 @@ export declare interface TypecheckOptions {
64
51
  }
65
52
  export declare interface TypecheckResult {
66
53
  diagnostics: TypecheckDiagnostic[]
54
+ failure?: string
67
55
  checkedFiles: string[]
68
56
  blockCount: number
69
57
  expressionCount: number