@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.
- package/dist/cli.js +1166 -1162
- package/dist/client-script-bundler.js +2 -1
- package/dist/client-script.d.ts +1 -12
- package/dist/client-script.js +2 -149
- package/dist/codemod.js +5 -0
- package/dist/compression.d.ts +11 -4
- package/dist/compression.js +57 -13
- package/dist/craft.js +400 -405
- package/dist/expressions.d.ts +0 -21
- package/dist/expressions.js +12 -1
- package/dist/find-body-tag.js +29 -17
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3 -0
- package/dist/page-action.d.ts +83 -0
- package/dist/page-action.js +64 -0
- package/dist/production-server.js +10 -3
- package/dist/pwa.js +304 -309
- package/dist/reactive-tracking.d.ts +81 -0
- package/dist/reactive-tracking.js +49 -0
- package/dist/reactivity.js +29 -23
- package/dist/runtime-injection.js +25 -39
- package/dist/serve.js +2 -1
- package/dist/signals-api.d.ts +18 -0
- package/dist/signals-api.js +26 -41
- package/dist/ssg.d.ts +8 -0
- package/dist/ssg.js +421 -426
- package/dist/state-management.js +2 -2
- package/dist/strip-literals.d.ts +40 -0
- package/dist/strip-literals.js +149 -0
- package/dist/stx-virtual-ts.d.ts +90 -1
- package/dist/stx-virtual-ts.js +90 -2
- package/dist/template-compiler.js +4 -2
- package/dist/template-hydrator.d.ts +1 -1
- package/dist/template-hydrator.js +13 -1
- package/dist/typecheck.d.ts +1 -1
- package/dist/typecheck.js +35 -7
- package/dist/types/config-types.d.ts +10 -3
- package/package.json +5 -5
package/dist/state-management.js
CHANGED
|
@@ -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"
|
|
235
|
-
window.__STX_DEVTOOLS__
|
|
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
|
+
}
|
package/dist/stx-virtual-ts.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/stx-virtual-ts.js
CHANGED
|
@@ -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)
|
|
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;
|
package/dist/typecheck.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
17
|
-
`.repeat(block.startLine - 1) : "")
|
|
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
|
-
}),
|
|
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.
|
|
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.
|
|
121
|
-
"@stacksjs/sanitizer": "0.2.
|
|
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.
|
|
123
|
+
"bun-plugin-stx": "0.2.171",
|
|
124
124
|
"bunfig": "^0.15.17",
|
|
125
|
-
"stx-router": "0.2.
|
|
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"
|