@stacksjs/stx 0.2.169 → 0.2.171

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.
@@ -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,70 @@ 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
+ * Server bindings a client block reaches for without a declared payload.
113
+ *
114
+ * The scraping bridge publishes a server binding when its name appears as a
115
+ * free identifier in the client source, so these DO arrive at runtime — they
116
+ * are just untyped, and their delivery depends on a textual coincidence rather
117
+ * than on anything the page states. Reporting them is #1868 ask 4, and it is
118
+ * only worth doing now that ask 2 gives them somewhere to go: warning people
119
+ * off the implicit path before the typed replacement existed would have been
120
+ * noise.
121
+ *
122
+ * A warning, not an error. These pages work; the fix is mechanical and the
123
+ * reward is types rather than correctness, so a gate that fails CI over it
124
+ * would be punishing an app for predating the feature.
125
+ *
126
+ * Empty once the page declares a payload, and empty when the client block does
127
+ * not actually mention any server name.
128
+ */
129
+ export declare function scrapedBridgeNames(serverCode: string, clientCode: string): string[];
45
130
  /**
46
131
  * Names declared by both a server block and a client/plain block.
47
132
  *
@@ -103,7 +188,11 @@ export declare const UNWRAP_HELPER: '__StxTemplateValue';
103
188
  * Mirrors what `serve.ts` puts in scope. Declaring these is what lets the
104
189
  * checker report a genuine typo instead of suppressing every unresolved name.
105
190
  */
106
- export declare const STX_SERVER_CONTEXT: readonly ['params', 'query', 'cookies', 'host', 'ip', 'setResponseStatus', 'setResponseHeader', 'notFound', 'request', 'url', 'definePageMeta'];
191
+ export declare const STX_SERVER_CONTEXT: readonly ['params', 'query', 'cookies', 'host', 'ip', 'setResponseStatus', 'setResponseHeader', 'notFound', 'request', 'url', 'definePageMeta', unknown, unknown, 'defineStore'];
192
+ /** Marker comment so a generated preamble is recognisable in a dumped buffer. */
193
+ export declare const PAYLOAD_COMMENT: 'stx: types for the declared client payload (#1868)';
194
+ /** The same, for a page still relying on the name-scraping bridge. */
195
+ export declare const SCRAPED_COMMENT: 'stx: untyped bridge names — declare a defineClientPayload to check these (#1868)';
107
196
  /**
108
197
  * Names the template engine puts in scope for every page.
109
198
  *
@@ -1,3 +1,4 @@
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;
3
4
  export function extractScriptBlocks(source) {
@@ -92,6 +93,14 @@ const UNWRAP_HELPER_DECL = `type ${UNWRAP_HELPER}<T> = T extends { readonly _isS
92
93
  export function substituteInterpolations(value) {
93
94
  return value.replace(/\{\{[\s\S]*?\}\}/g, INTERPOLATION_PLACEHOLDER).replace(/\{!![\s\S]*?!!\}/g, INTERPOLATION_PLACEHOLDER);
94
95
  }
96
+ export function substituteInterpolationsInPlace(code) {
97
+ return code.replace(/\{\{[\s\S]*?\}\}|\{!![\s\S]*?!!\}/g, (match) => {
98
+ const newlines = match.split(`
99
+ `).length - 1, width = match.length - newlines;
100
+ return `0${" ".repeat(Math.max(0, width - 1))}${`
101
+ `.repeat(newlines)}`;
102
+ });
103
+ }
95
104
  const NON_EXPRESSION_ATTRS = new Set([
96
105
  "x-cloak",
97
106
  "x-ref",
@@ -120,7 +129,7 @@ export function extractTemplateExpressions(source) {
120
129
  }
121
130
  DIRECTIVE_ATTR_RE.lastIndex = 0;
122
131
  while ((m = DIRECTIVE_ATTR_RE.exec(masked)) !== null) {
123
- const name = m[1], value = m[3] ?? "", base = name.replace(/\..*$/, "");
132
+ const name = m[1], value = m[3] ?? "", base = name.replace(/\..*$/, "").replace(/^([^:].*?):.*$/, "$1");
124
133
  if (NON_EXPRESSION_ATTRS.has(base) || NON_EXPRESSION_ATTRS.has(name))
125
134
  continue;
126
135
  if (!value.trim())
@@ -229,12 +238,91 @@ export const STX_SERVER_CONTEXT = [
229
238
  "notFound",
230
239
  "request",
231
240
  "url",
232
- "definePageMeta"
241
+ "definePageMeta",
242
+ "defineClientPayload",
243
+ "defineStore"
233
244
  ];
234
245
  export function serverContextDeclarations() {
235
246
  return STX_SERVER_CONTEXT.map((name) => `declare var ${name}: any`).join(`
236
247
  `);
237
248
  }
249
+ export function extractClientPayloadNames(serverCode) {
250
+ const call = /\bdefineClientPayload\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
251
+ let match, found = !1;
252
+ const names = new Set, searchable = stripCommentsAndLiterals(serverCode);
253
+ while ((match = call.exec(searchable)) !== null) {
254
+ found = !0;
255
+ for (const entry of splitPayloadEntries(match[1])) {
256
+ const name = entry.split(":")[0].trim();
257
+ if (/^[A-Za-z_$][\w$]*$/.test(name))
258
+ names.add(name);
259
+ }
260
+ }
261
+ return found ? [...names] : null;
262
+ }
263
+ function splitPayloadEntries(body) {
264
+ const entries = [];
265
+ let depth = 0, current = "";
266
+ for (const char of body) {
267
+ if (char === "," && depth === 0) {
268
+ entries.push(current);
269
+ current = "";
270
+ continue;
271
+ }
272
+ if (char === "(" || char === "[" || char === "{")
273
+ depth++;
274
+ else if (char === ")" || char === "]" || char === "}")
275
+ depth--;
276
+ current += char;
277
+ }
278
+ entries.push(current);
279
+ return entries.filter((entry) => entry.trim().length > 0);
280
+ }
281
+ export function splitTopLevelImports(code) {
282
+ const importLine = /^[ \t]*import\b[\s\S]*?(?:from\s*['"][^'"]*['"]|['"][^'"]*['"])[ \t]*;?[ \t]*$/gm, imports = [], body = code.replace(importLine, (statement) => {
283
+ imports.push(statement.trim());
284
+ return statement.split(`
285
+ `).map(() => "").join(`
286
+ `);
287
+ });
288
+ return { imports: imports.join(`
289
+ `), body };
290
+ }
291
+ export function clientPayloadDeclarations(serverCode) {
292
+ const published = extractClientPayloadNames(serverCode);
293
+ if (!published) {
294
+ const scraped = collectBlockDeclarations(serverCode);
295
+ if (scraped.length === 0)
296
+ return "";
297
+ return [
298
+ "",
299
+ `// ${SCRAPED_COMMENT}`,
300
+ ...scraped.map((name) => `declare var ${name}: any`)
301
+ ].join(`
302
+ `);
303
+ }
304
+ if (published.length === 0)
305
+ return "";
306
+ const { imports, body } = splitTopLevelImports(serverCode), scopeBody = body.replace(/^[ \t]*export[ \t]+(?=(?:const|let|var|function|async|class)\b)/gm, ""), parts = [
307
+ "",
308
+ `// ${PAYLOAD_COMMENT}`
309
+ ];
310
+ if (imports)
311
+ parts.push(imports);
312
+ 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)}]`));
313
+ return parts.join(`
314
+ `);
315
+ }
316
+ export function scrapedBridgeNames(serverCode, clientCode) {
317
+ if (extractClientPayloadNames(serverCode) !== null)
318
+ return [];
319
+ const searchable = stripCommentsAndLiterals(clientCode);
320
+ return collectBlockDeclarations(serverCode).filter((name) => {
321
+ return new RegExp(`(?<![.#\\w$])${name}(?![\\w$])`).test(searchable);
322
+ });
323
+ }
324
+ 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)";
325
+ const PAYLOAD_SCOPE = "__stxServerScope", PAYLOAD_TYPE = "__StxClientPayload", PAYLOAD_NAMESPACE = "__StxServerBlock";
238
326
  export const STX_TEMPLATE_GLOBALS = [
239
327
  "$env",
240
328
  "$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. */
package/dist/typecheck.js CHANGED
@@ -6,15 +6,20 @@ import {
6
6
  buildVirtualTypeScript,
7
7
  extractScriptBlocks,
8
8
  resolvePosition,
9
- serverContextDeclarations
9
+ clientPayloadDeclarations,
10
+ scrapedBridgeNames,
11
+ serverContextDeclarations,
12
+ substituteInterpolationsInPlace
10
13
  } from "./stx-virtual-ts.js";
11
14
  export {
12
15
  extractScriptBlocks,
13
16
  extractTemplateExpressions
14
17
  } from "./stx-virtual-ts.js";
15
- export function buildVirtualSource(block) {
16
- return `${(block.startLine > 1 ? `
17
- `.repeat(block.startLine - 1) : "") + block.code}
18
+ export function buildVirtualSource(block, serverCode = "") {
19
+ const leadingNewlines = block.startLine > 1 ? `
20
+ `.repeat(block.startLine - 1) : "", code = substituteInterpolationsInPlace(block.code), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode) : "";
21
+ return `${leadingNewlines + code}
22
+ ${payload}
18
23
  export {}
19
24
  `;
20
25
  }
@@ -35,7 +40,7 @@ export function findRuntimeTypeDeclarations() {
35
40
  }
36
41
  const TSC_LINE_RE = /^(.*?)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.*)$/;
37
42
  export async function typecheckStxFiles(files, options = {}) {
38
- const checkClient = options.client !== !1, checkServer = options.server !== !1, checkTemplates = options.templates !== !1, virtualFiles = new Map, checkedFiles = [];
43
+ const checkClient = options.client !== !1, checkServer = options.server !== !1, checkTemplates = options.templates !== !1, virtualFiles = new Map, scrapedByFile = new Map, checkedFiles = [];
39
44
  let blockCount = 0, expressionCount = 0;
40
45
  const runtimeTypes = findRuntimeTypeDeclarations();
41
46
  for (const file of files) {
@@ -45,7 +50,14 @@ export async function typecheckStxFiles(files, options = {}) {
45
50
  if (b.kind === "client")
46
51
  return checkClient;
47
52
  return !1;
48
- }), templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
53
+ }), serverCode = extractScriptBlocks(source).filter((block) => block.kind === "server").map((block) => block.code).join(`
54
+ `);
55
+ if (checkClient && serverCode) {
56
+ const clientBlocks = blocks.filter((block) => block.kind === "client"), names = [...new Set(clientBlocks.flatMap((block) => scrapedBridgeNames(serverCode, block.code)))];
57
+ if (names.length > 0)
58
+ scrapedByFile.set(file, { names, line: clientBlocks[0].startLine });
59
+ }
60
+ const templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
49
61
  if (blocks.length === 0 && expressions === 0)
50
62
  continue;
51
63
  checkedFiles.push(file);
@@ -53,7 +65,7 @@ export async function typecheckStxFiles(files, options = {}) {
53
65
  expressionCount += expressions;
54
66
  blocks.forEach((block, i) => {
55
67
  virtualFiles.set(virtualPathFor(file, block.kind, i), {
56
- source: buildVirtualSource(block),
68
+ source: buildVirtualSource(block, serverCode),
57
69
  origin: file,
58
70
  kind: block.kind
59
71
  });
@@ -122,6 +134,8 @@ export async function typecheckStxFiles(files, options = {}) {
122
134
  const at = resolvePosition(entry.virtual, line, column);
123
135
  if (!at?.expression)
124
136
  continue;
137
+ if (Number(code) === 2774)
138
+ continue;
125
139
  line = at.line;
126
140
  column = at.column;
127
141
  expression = at.expression.code.trim();
@@ -137,6 +151,20 @@ export async function typecheckStxFiles(files, options = {}) {
137
151
  expression
138
152
  });
139
153
  }
154
+ if (checkClient)
155
+ for (const [file, scraped] of scrapedByFile) {
156
+ if (scraped.names.length === 0)
157
+ continue;
158
+ diagnostics.push({
159
+ file,
160
+ line: scraped.line,
161
+ column: 1,
162
+ code: 0,
163
+ message: `${scraped.names.length === 1 ? "1 value reaches" : `${scraped.names.length} values reach`} this block on the implicit bridge and ${scraped.names.length === 1 ? "is" : "are"} untyped: ${scraped.names.join(", ")}. Declare them with defineClientPayload({ ${scraped.names.join(", ")} }) in the server block to have them checked.`,
164
+ category: "warning",
165
+ blockKind: "client"
166
+ });
167
+ }
140
168
  const seen = new Set, unique = diagnostics.filter((d) => {
141
169
  const key = `${d.file}:${d.line}:${d.column}:${d.code}:${d.message}`;
142
170
  if (seen.has(key))
@@ -344,6 +344,16 @@ export interface StrictModeConfig {
344
344
  failOnViolation?: boolean;
345
345
  /** Pattern strings to allow through validation (matched against message or regex source) */
346
346
  allowPatterns?: string[];
347
+ /**
348
+ * What to do when a `<script client>` block does not bundle.
349
+ *
350
+ * `'warn'` (default) ships the ORIGINAL, unbundled source so the page still
351
+ * renders — right for a dev server, and why the error overlay exists.
352
+ * `'error'` refuses instead, for anyone who would rather nothing rendered
353
+ * than a page whose bindings quietly do nothing. `stx build` fails on a
354
+ * bundle error either way. See #1884.
355
+ */
356
+ bundlerFallback?: 'warn' | 'error';
347
357
  }
348
358
  /**
349
359
  * Real-time broadcasting configuration.
@@ -655,9 +665,6 @@ export interface StxConfig {
655
665
  * rendered than a page whose bindings quietly do nothing. `stx build` fails on
656
666
  * a bundle error either way. See #1884.
657
667
  */
658
- strict?: {
659
- bundlerFallback?: 'warn' | 'error';
660
- };
661
668
  /** Directory to store cached templates, defaults to `cache` inside `stateDir` */
662
669
  cachePath: string;
663
670
  /** Custom directives registered by the user */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.169",
4
+ "version": "0.2.171",
5
5
  "description": "A performant UI Framework. Powered by Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -117,12 +117,12 @@
117
117
  "dependencies": {
118
118
  "@cwcss/crosswind": "^0.2.14",
119
119
  "@stacksjs/clapp": "^0.2.0",
120
- "@stacksjs/desktop": "0.2.169",
121
- "@stacksjs/sanitizer": "0.2.169",
120
+ "@stacksjs/desktop": "0.2.171",
121
+ "@stacksjs/sanitizer": "0.2.171",
122
122
  "@stacksjs/ts-i18n": "^0.1.11",
123
- "bun-plugin-stx": "0.2.169",
123
+ "bun-plugin-stx": "0.2.171",
124
124
  "bunfig": "^0.15.17",
125
- "stx-router": "0.2.169",
125
+ "stx-router": "0.2.171",
126
126
  "ts-broadcasting": "^0.0.9",
127
127
  "ts-images": "^0.1.9",
128
128
  "ts-syntax-highlighter": "^0.2.1"