@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.
- package/dist/cli.js +1263 -1168
- 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.d.ts +1 -24
- package/dist/codemod.js +61 -9
- package/dist/compression.d.ts +11 -4
- package/dist/compression.js +52 -13
- package/dist/craft.js +489 -415
- 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 +8 -0
- package/dist/index.js +2 -0
- package/dist/page-action.d.ts +83 -0
- package/dist/page-action.js +64 -0
- package/dist/production-builder.js +1 -0
- package/dist/production-server.js +10 -3
- package/dist/pwa.js +398 -324
- package/dist/reactive-tracking.d.ts +81 -0
- package/dist/reactive-tracking.js +49 -0
- package/dist/reactivity.js +29 -23
- package/dist/render.js +5 -18
- package/dist/runtime-injection.js +25 -39
- package/dist/script-validation.d.ts +0 -7
- package/dist/script-validation.js +6 -4
- package/dist/signals-api.d.ts +18 -0
- package/dist/signals-api.js +26 -41
- package/dist/signals.js +93 -15
- package/dist/ssg.d.ts +8 -0
- package/dist/ssg.js +515 -441
- 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 +103 -1
- package/dist/stx-virtual-ts.js +124 -7
- 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 +2 -14
- package/dist/typecheck.js +133 -11
- package/dist/types/config-types.d.ts +10 -3
- package/dist/variable-extractor.js +1 -1
- 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,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
|
*
|
package/dist/stx-virtual-ts.js
CHANGED
|
@@ -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(
|
|
8
|
-
const attrs = (match[1] || "").trim(),
|
|
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",
|
|
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)
|
|
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. */
|
|
@@ -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
|