@stacksjs/stx 0.2.175 → 0.2.177
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y.d.ts +72 -2
- package/dist/a11y.js +25 -10
- package/dist/builtins/attrs.d.ts +19 -0
- package/dist/builtins/attrs.js +18 -0
- package/dist/builtins/safe-image.js +2 -1
- package/dist/builtins/stx-image.js +2 -1
- package/dist/builtins/stx-link.js +2 -1
- package/dist/cli-flags.d.ts +34 -0
- package/dist/cli-flags.js +20 -0
- package/dist/cli.js +1132 -895
- package/dist/client-script.js +2 -1
- package/dist/component-registry.d.ts +9 -0
- package/dist/component-renderer.js +20 -1
- package/dist/composables/use-fetch.d.ts +6 -1
- package/dist/composables/use-fetch.js +40 -18
- package/dist/composables/use-query.d.ts +7 -2
- package/dist/cookie-serialize.d.ts +40 -0
- package/dist/cookie-serialize.js +18 -0
- package/dist/craft.js +670 -442
- package/dist/crosswind-config.d.ts +13 -0
- package/dist/crosswind-config.js +6 -2
- package/dist/dev-server/crosswind.js +2 -1
- package/dist/edge-runtime.d.ts +2 -9
- package/dist/edge-runtime.js +2 -16
- package/dist/includes.js +8 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/page-action.d.ts +10 -20
- package/dist/page-action.js +33 -9
- package/dist/process.js +2 -1
- package/dist/production-server.js +8 -7
- package/dist/props.d.ts +51 -8
- package/dist/pwa.js +637 -409
- package/dist/reactive.js +2 -2
- package/dist/resolve-stx-targets.d.ts +49 -0
- package/dist/resolve-stx-targets.js +38 -0
- package/dist/runtime-globals.d.ts +24 -0
- package/dist/runtime-globals.js +16 -0
- package/dist/runtime-injection.d.ts +36 -0
- package/dist/runtime-injection.js +14 -0
- package/dist/server-bindings.js +3 -0
- package/dist/signal-processing.js +38 -7
- package/dist/signals.js +157 -35
- package/dist/ssg.d.ts +23 -0
- package/dist/ssg.js +615 -383
- package/dist/ssr.js +2 -16
- package/dist/stx-virtual-ts.d.ts +72 -4
- package/dist/stx-virtual-ts.js +111 -5
- package/dist/stx.d.ts +66 -7
- package/dist/template-hydrator.d.ts +1 -1
- package/dist/template-hydrator.js +6 -6
- package/dist/theme-tokens.d.ts +88 -0
- package/dist/theme-tokens.js +71 -0
- package/dist/typecheck.d.ts +6 -1
- package/dist/typecheck.js +102 -67
- package/dist/types/config-types.d.ts +24 -0
- package/dist/utils.js +5 -4
- package/package.json +5 -5
- package/stx.d.ts +66 -7
package/dist/ssr.js
CHANGED
|
@@ -4,6 +4,7 @@ import { errorLogger } from "./error-handling.js";
|
|
|
4
4
|
import { processDirectives } from "./process.js";
|
|
5
5
|
import { extractVariables } from "./utils.js";
|
|
6
6
|
import { patternToRegex } from "stx-router";
|
|
7
|
+
import { serializeSetCookie } from "./cookie-serialize.js";
|
|
7
8
|
|
|
8
9
|
export class MemorySessionStore {
|
|
9
10
|
store = new Map;
|
|
@@ -89,22 +90,7 @@ function parseCookies(cookieHeader) {
|
|
|
89
90
|
}, {});
|
|
90
91
|
}
|
|
91
92
|
function serializeCookie(name, value, options = {}) {
|
|
92
|
-
|
|
93
|
-
if (options.maxAge !== void 0)
|
|
94
|
-
cookie += `; Max-Age=${options.maxAge}`;
|
|
95
|
-
if (options.expires)
|
|
96
|
-
cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
97
|
-
if (options.path)
|
|
98
|
-
cookie += `; Path=${options.path}`;
|
|
99
|
-
if (options.domain)
|
|
100
|
-
cookie += `; Domain=${options.domain}`;
|
|
101
|
-
if (options.secure)
|
|
102
|
-
cookie += "; Secure";
|
|
103
|
-
if (options.httpOnly)
|
|
104
|
-
cookie += "; HttpOnly";
|
|
105
|
-
if (options.sameSite)
|
|
106
|
-
cookie += `; SameSite=${options.sameSite}`;
|
|
107
|
-
return cookie;
|
|
93
|
+
return serializeSetCookie(name, value, options);
|
|
108
94
|
}
|
|
109
95
|
function generateCsrfToken() {
|
|
110
96
|
return crypto.randomBytes(32).toString("hex");
|
package/dist/stx-virtual-ts.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Pull the script blocks out of a `.stx` source, with the line each body starts on.
|
|
3
3
|
*
|
|
4
4
|
* `<script server>` and `<script client>` are recognised by attribute. A bare
|
|
5
|
-
* `<script>` is reported as `plain
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `<script>` is reported as `plain` so a caller can tell which tag the author
|
|
6
|
+
* actually wrote — NOT so it can be skipped. It is a client block; `client` is
|
|
7
|
+
* an explicit alias for the default, not a separate kind (stacksjs/stx#1920).
|
|
8
8
|
*/
|
|
9
9
|
export declare function extractScriptBlocks(source: string): ScriptBlock[];
|
|
10
10
|
/**
|
|
@@ -45,6 +45,30 @@ export declare function substituteInterpolations(value: string): string;
|
|
|
45
45
|
* fit and would shift the line.
|
|
46
46
|
*/
|
|
47
47
|
export declare function substituteInterpolationsInPlace(code: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Blank stx directives that a script body writes to generate JavaScript.
|
|
50
|
+
*
|
|
51
|
+
* A template can loop inside a script — building a lookup table from server
|
|
52
|
+
* data is the obvious use — and the directive is expanded away long before the
|
|
53
|
+
* browser sees it:
|
|
54
|
+
*
|
|
55
|
+
* const data = {
|
|
56
|
+
* @foreach(features as f)
|
|
57
|
+
* '{{ f.id }}': { title: '{{ f.title }}' },
|
|
58
|
+
* @endforeach
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* To TypeScript the directive line is a syntax error, and one of those mutes
|
|
62
|
+
* semantic checking for the WHOLE program, not just the file. So checking bare
|
|
63
|
+
* `<script>` blocks (#1920) made a valid page report 37 invented syntax errors
|
|
64
|
+
* — and a checker that invents errors gets muted, which is the failure mode
|
|
65
|
+
* this file exists to avoid.
|
|
66
|
+
*
|
|
67
|
+
* Blanked rather than removed so every line number after it still points at the
|
|
68
|
+
* line the author wrote. Only a directive ALONE on its line is matched: that is
|
|
69
|
+
* how they are written, and it cannot swallow code sharing the line.
|
|
70
|
+
*/
|
|
71
|
+
export declare function blankScriptDirectives(code: string): string;
|
|
48
72
|
/**
|
|
49
73
|
* Every checkable expression in the markup, with the position it occupies.
|
|
50
74
|
*
|
|
@@ -107,7 +131,23 @@ export declare function splitTopLevelImports(code: string): { imports: string, b
|
|
|
107
131
|
* So the incentive runs the right way: declare a payload and the names are
|
|
108
132
|
* checked against the server block, or do not and they are `any`.
|
|
109
133
|
*/
|
|
110
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Top-level `interface X {…}` / `type X = …` statements, with their source text.
|
|
136
|
+
*
|
|
137
|
+
* The payload projection wraps the whole server body in a scope function so the
|
|
138
|
+
* checker can infer each published value's type from real code. That works for
|
|
139
|
+
* VALUES and traps TYPES: an `interface AutofixState` declared in
|
|
140
|
+
* `<script server>` lives inside that function, so a `<script client>` block in
|
|
141
|
+
* the same file naming it got `Cannot find name 'AutofixState'` — an error about
|
|
142
|
+
* code that is correct and runs (stacksjs/stx#1924).
|
|
143
|
+
*
|
|
144
|
+
* Scanned at depth zero so an interface declared inside a helper is not lifted
|
|
145
|
+
* out of the scope it belongs to. The brace matcher walks the real source rather
|
|
146
|
+
* than a blanked copy, because the declaration's BODY is what has to be carried
|
|
147
|
+
* across, not just its name.
|
|
148
|
+
*/
|
|
149
|
+
export declare function extractTypeDeclarations(code: string): Array<{ name: string, text: string }>;
|
|
150
|
+
export declare function clientPayloadDeclarations(serverCode: string, clientCode?: string): string;
|
|
111
151
|
/**
|
|
112
152
|
* Declarations at the TOP LEVEL of a block, ignoring anything nested.
|
|
113
153
|
*
|
|
@@ -157,6 +197,33 @@ export declare function crossScopeCollisions(blocks: readonly ScriptBlock[]): st
|
|
|
157
197
|
* `const`, and that form is documented stx syntax, not a mistake.
|
|
158
198
|
*/
|
|
159
199
|
export declare function expressionStatement(expression: TemplateExpression): { text: string, prefixLength: number } | null;
|
|
200
|
+
/**
|
|
201
|
+
* Rewrite relative module specifiers to absolute paths against the real file.
|
|
202
|
+
*
|
|
203
|
+
* A block's virtual buffer is written into the state directory, and TypeScript
|
|
204
|
+
* resolves a relative specifier against the file that CONTAINS it — so
|
|
205
|
+
* `../target`, written in `pages/deep/rel.stx`, was looked for next to
|
|
206
|
+
* `.stx/typecheck/` and reported as a missing module (stacksjs/stx#1928).
|
|
207
|
+
*
|
|
208
|
+
* A `paths` alias to the same file resolved fine, because those go through the
|
|
209
|
+
* absolute `baseUrl` the checker emits. That asymmetry is the whole bug: a
|
|
210
|
+
* relative import is the first thing anyone writes and what every editor's
|
|
211
|
+
* auto-import produces, so an app hitting it either rewrites correct imports
|
|
212
|
+
* into aliases to appease the checker, or mutes the checker.
|
|
213
|
+
*
|
|
214
|
+
* Rewriting each specifier is preferred over pointing the program at the source
|
|
215
|
+
* directory — `rootDirs`, the built-in answer for a virtual layout, MERGES its
|
|
216
|
+
* entries into one namespace, so `./util` from one checked page could silently
|
|
217
|
+
* resolve to a different page's `util` when both directories are roots. Each
|
|
218
|
+
* buffer knows exactly one origin, so resolving per file cannot cross-talk.
|
|
219
|
+
*
|
|
220
|
+
* Textual rather than parsed, and deliberately: `Bun.Transpiler.scanImports`
|
|
221
|
+
* erases `import type`, which is the form a type-checker cares about most. The
|
|
222
|
+
* cost is that the literal text `from './x'` inside a string or comment is
|
|
223
|
+
* rewritten too — inert, since this buffer is never executed, and it cannot
|
|
224
|
+
* change a line count, so every diagnostic still lands on the author's line.
|
|
225
|
+
*/
|
|
226
|
+
export declare function absolutizeRelativeSpecifiers(code: string, originDir: string): string;
|
|
160
227
|
/**
|
|
161
228
|
* Build one line-aligned TypeScript buffer for a whole `.stx` file.
|
|
162
229
|
*
|
|
@@ -255,6 +322,7 @@ export declare interface TemplateBinding {
|
|
|
255
322
|
}
|
|
256
323
|
export declare interface BuildVirtualOptions {
|
|
257
324
|
templateExpressions?: boolean
|
|
325
|
+
originDir?: string
|
|
258
326
|
globals?: boolean
|
|
259
327
|
runtimeGlobals?: boolean
|
|
260
328
|
}
|
package/dist/stx-virtual-ts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { stripCommentsAndLiterals } from "./strip-literals.js";
|
|
2
3
|
import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
|
|
3
4
|
const SCRIPT_RE = /<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi;
|
|
@@ -105,6 +106,42 @@ export function substituteInterpolationsInPlace(code) {
|
|
|
105
106
|
`.repeat(newlines)}`;
|
|
106
107
|
});
|
|
107
108
|
}
|
|
109
|
+
const SCRIPT_BODY_DIRECTIVES = [
|
|
110
|
+
"foreach",
|
|
111
|
+
"endforeach",
|
|
112
|
+
"forelse",
|
|
113
|
+
"endforelse",
|
|
114
|
+
"for",
|
|
115
|
+
"endfor",
|
|
116
|
+
"while",
|
|
117
|
+
"endwhile",
|
|
118
|
+
"empty",
|
|
119
|
+
"if",
|
|
120
|
+
"elseif",
|
|
121
|
+
"else",
|
|
122
|
+
"endif",
|
|
123
|
+
"unless",
|
|
124
|
+
"endunless",
|
|
125
|
+
"isset",
|
|
126
|
+
"endisset",
|
|
127
|
+
"switch",
|
|
128
|
+
"case",
|
|
129
|
+
"default",
|
|
130
|
+
"endswitch",
|
|
131
|
+
"auth",
|
|
132
|
+
"endauth",
|
|
133
|
+
"guest",
|
|
134
|
+
"endguest",
|
|
135
|
+
"env",
|
|
136
|
+
"endenv",
|
|
137
|
+
"once",
|
|
138
|
+
"endonce",
|
|
139
|
+
"break",
|
|
140
|
+
"continue"
|
|
141
|
+
], SCRIPT_BODY_DIRECTIVE_RE = new RegExp(String.raw`^[ \t]*@(?:${SCRIPT_BODY_DIRECTIVES.join("|")})\b[ \t]*(\([^\n)]*\))?[ \t]*$`, "gm");
|
|
142
|
+
export function blankScriptDirectives(code) {
|
|
143
|
+
return code.replace(SCRIPT_BODY_DIRECTIVE_RE, (match) => match.replace(/[^\n]/g, " "));
|
|
144
|
+
}
|
|
108
145
|
const NON_EXPRESSION_ATTRS = new Set([
|
|
109
146
|
"x-cloak",
|
|
110
147
|
"x-ref",
|
|
@@ -294,27 +331,87 @@ export function splitTopLevelImports(code) {
|
|
|
294
331
|
return { imports: imports.join(`
|
|
295
332
|
`), body };
|
|
296
333
|
}
|
|
297
|
-
export function
|
|
298
|
-
const
|
|
334
|
+
export function extractTypeDeclarations(code) {
|
|
335
|
+
const searchable = stripCommentsAndLiterals(code), depthAt = Array(searchable.length);
|
|
336
|
+
let depth = 0;
|
|
337
|
+
for (let i = 0;i < searchable.length; i++) {
|
|
338
|
+
const c = searchable[i];
|
|
339
|
+
if (c === "}" || c === ")" || c === "]")
|
|
340
|
+
depth = Math.max(0, depth - 1);
|
|
341
|
+
depthAt[i] = depth;
|
|
342
|
+
if (c === "{" || c === "(" || c === "[")
|
|
343
|
+
depth++;
|
|
344
|
+
}
|
|
345
|
+
const found = [], start = /^[ \t]*(?:export[ \t]+)?(interface|type)[ \t]+([A-Za-z_$][\w$]*)/gm;
|
|
346
|
+
let match;
|
|
347
|
+
while ((match = start.exec(searchable)) !== null) {
|
|
348
|
+
if (depthAt[match.index] !== 0)
|
|
349
|
+
continue;
|
|
350
|
+
const kind = match[1], name = match[2];
|
|
351
|
+
let end;
|
|
352
|
+
if (kind === "interface") {
|
|
353
|
+
const open = searchable.indexOf("{", match.index);
|
|
354
|
+
if (open === -1)
|
|
355
|
+
continue;
|
|
356
|
+
let depth = 0;
|
|
357
|
+
end = -1;
|
|
358
|
+
for (let i = open;i < searchable.length; i++)
|
|
359
|
+
if (searchable[i] === "{")
|
|
360
|
+
depth++;
|
|
361
|
+
else if (searchable[i] === "}" && --depth === 0) {
|
|
362
|
+
end = i + 1;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (end === -1)
|
|
366
|
+
continue;
|
|
367
|
+
} else {
|
|
368
|
+
let depth = 0;
|
|
369
|
+
end = searchable.length;
|
|
370
|
+
for (let i = searchable.indexOf("=", match.index) + 1;i < searchable.length; i++) {
|
|
371
|
+
const c = searchable[i];
|
|
372
|
+
if (c === "{" || c === "[" || c === "(" || c === "<")
|
|
373
|
+
depth++;
|
|
374
|
+
else if (c === "}" || c === "]" || c === ")" || c === ">")
|
|
375
|
+
depth--;
|
|
376
|
+
else if (depth <= 0 && (c === ";" || c === `
|
|
377
|
+
` && !/[=|&,<([{]\s*$/.test(searchable.slice(Math.max(0, i - 40), i)))) {
|
|
378
|
+
end = c === ";" ? i + 1 : i;
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
found.push({ name, text: code.slice(match.index, end).replace(/^[ \t]*export[ \t]+/, "") });
|
|
384
|
+
start.lastIndex = end;
|
|
385
|
+
}
|
|
386
|
+
return found;
|
|
387
|
+
}
|
|
388
|
+
function declaredTypeNames(code) {
|
|
389
|
+
return new Set(extractTypeDeclarations(code).map((entry) => entry.name));
|
|
390
|
+
}
|
|
391
|
+
export function clientPayloadDeclarations(serverCode, clientCode = "") {
|
|
392
|
+
const published = extractClientPayloadNames(serverCode), ownTypes = declaredTypeNames(clientCode), serverTypes = extractTypeDeclarations(serverCode).filter((entry) => !ownTypes.has(entry.name)).map((entry) => entry.text);
|
|
299
393
|
if (!published) {
|
|
300
394
|
const scraped = collectBlockDeclarations(serverCode);
|
|
301
|
-
if (scraped.length === 0)
|
|
395
|
+
if (scraped.length === 0 && serverTypes.length === 0)
|
|
302
396
|
return "";
|
|
303
397
|
return [
|
|
304
398
|
"",
|
|
305
399
|
`// ${SCRAPED_COMMENT}`,
|
|
400
|
+
...serverTypes,
|
|
306
401
|
...scraped.map((name) => `declare var ${name}: any`)
|
|
307
402
|
].join(`
|
|
308
403
|
`);
|
|
309
404
|
}
|
|
310
405
|
if (published.length === 0)
|
|
311
|
-
return ""
|
|
406
|
+
return serverTypes.length > 0 ? ["", `// ${PAYLOAD_COMMENT}`, ...serverTypes].join(`
|
|
407
|
+
`) : "";
|
|
312
408
|
const { imports, body } = splitTopLevelImports(serverCode), scopeBody = body.replace(/^[ \t]*export[ \t]+(?=(?:const|let|var|function|async|class)\b)/gm, ""), parts = [
|
|
313
409
|
"",
|
|
314
410
|
`// ${PAYLOAD_COMMENT}`
|
|
315
411
|
];
|
|
316
412
|
if (imports)
|
|
317
413
|
parts.push(imports);
|
|
414
|
+
parts.push(...serverTypes);
|
|
318
415
|
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
416
|
return parts.join(`
|
|
320
417
|
`);
|
|
@@ -390,11 +487,20 @@ export function expressionStatement(expression) {
|
|
|
390
487
|
const prefix = ";void ((";
|
|
391
488
|
return { text: `${prefix}${code}));`, prefixLength: prefix.length };
|
|
392
489
|
}
|
|
490
|
+
const RELATIVE_SPECIFIER_RE = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])(\.{1,2}\/[^'"\n]*)\2/g;
|
|
491
|
+
export function absolutizeRelativeSpecifiers(code, originDir) {
|
|
492
|
+
if (!originDir)
|
|
493
|
+
return code;
|
|
494
|
+
return code.replace(RELATIVE_SPECIFIER_RE, (_whole, prefix, quote, specifier) => {
|
|
495
|
+
const absolute = path.resolve(originDir, specifier).replace(/\\/g, "/");
|
|
496
|
+
return `${prefix}${quote}${absolute}${quote}`;
|
|
497
|
+
});
|
|
498
|
+
}
|
|
393
499
|
export function buildVirtualTypeScript(source, options = {}) {
|
|
394
500
|
const blocks = extractScriptBlocks(source), sourceLines = source.split(`
|
|
395
501
|
`), lines = Array.from({ length: sourceLines.length }, () => ""), lineMap = new Map;
|
|
396
502
|
for (const block of blocks)
|
|
397
|
-
block.code.split(`
|
|
503
|
+
absolutizeRelativeSpecifiers(block.code, options.originDir ?? "").split(`
|
|
398
504
|
`).forEach((text, i) => {
|
|
399
505
|
const index = block.startLine - 1 + i;
|
|
400
506
|
if (index < lines.length)
|
package/dist/stx.d.ts
CHANGED
|
@@ -199,11 +199,42 @@ declare function useSearchParams(): {
|
|
|
199
199
|
// Data fetching
|
|
200
200
|
// ============================================================================
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
/** How one run of a request should behave. */
|
|
203
|
+
interface StxRefetchOptions {
|
|
204
|
+
/**
|
|
205
|
+
* Refresh without touching `loading` or clearing the current error.
|
|
206
|
+
*
|
|
207
|
+
* A poll refreshes data that is already on screen, so driving the first-load
|
|
208
|
+
* state from it puts a spinner over a populated view on every tick — the
|
|
209
|
+
* reason a polling view could not use these primitives at all (#1929). Bind
|
|
210
|
+
* `isFetching` for a subtle in-flight indicator instead.
|
|
211
|
+
*/
|
|
212
|
+
background?: boolean
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface StxFetchResult<T> {
|
|
203
216
|
data: StxSignal<T | null>
|
|
217
|
+
/** True while a first-load request is in flight. A background one leaves it alone. */
|
|
204
218
|
loading: StxSignal<boolean>
|
|
219
|
+
/** True while ANY request is in flight, background ones included. */
|
|
220
|
+
isFetching: StxSignal<boolean>
|
|
205
221
|
error: StxSignal<Error | null>
|
|
206
|
-
refetch: () => Promise<void>
|
|
222
|
+
refetch: (_options?: StxRefetchOptions) => Promise<void>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* `useQuery` returns more than `useFetch` does, and declaring one shape for
|
|
227
|
+
* both hid it: `isStale` and `invalidate` existed at runtime and were invisible
|
|
228
|
+
* to the type, so reading either was an error on a value that was really there
|
|
229
|
+
* (#1929). Extending rather than widening the shared interface, because the
|
|
230
|
+
* inverse mistake is worse — declaring `invalidate` on `useFetch` would
|
|
231
|
+
* type-check a call to something that is undefined at runtime.
|
|
232
|
+
*/
|
|
233
|
+
interface StxQueryResult<T> extends StxFetchResult<T> {
|
|
234
|
+
/** True when cached data is on screen while a revalidation runs. */
|
|
235
|
+
isStale: StxSignal<boolean>
|
|
236
|
+
/** Drop this key from the cache and refetch. */
|
|
237
|
+
invalidate: (_options?: StxRefetchOptions) => Promise<void>
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
interface StxMutationResult<T> {
|
|
@@ -213,7 +244,7 @@ interface StxMutationResult<T> {
|
|
|
213
244
|
mutate: (body?: unknown) => Promise<T>
|
|
214
245
|
}
|
|
215
246
|
|
|
216
|
-
declare function useFetch<T = any>(_url: string, _options?: any):
|
|
247
|
+
declare function useFetch<T = any>(_url: string, _options?: any): StxFetchResult<T>
|
|
217
248
|
declare function useQuery<T = any>(_url: string, _options?: any): StxQueryResult<T>
|
|
218
249
|
declare function useMutation<T = any>(_url: string, _options?: any): StxMutationResult<T>
|
|
219
250
|
|
|
@@ -271,17 +302,45 @@ declare function useOptimistic<T = any, A = any>(
|
|
|
271
302
|
// DOM utilities
|
|
272
303
|
// ============================================================================
|
|
273
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Options the runtime reads, including the target it binds to.
|
|
307
|
+
*
|
|
308
|
+
* The target travels HERE rather than in a leading parameter, because that is
|
|
309
|
+
* what the implementation does: `var target = (options && options.target) || window`.
|
|
310
|
+
*/
|
|
311
|
+
interface StxEventListenerOptions {
|
|
312
|
+
/** Element, selector, or document/window. Defaults to `window`. */
|
|
313
|
+
target?: Window | Document | HTMLElement | string | null
|
|
314
|
+
capture?: boolean
|
|
315
|
+
passive?: boolean
|
|
316
|
+
once?: boolean
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* Event first, then handler, then options (stacksjs/stx#1923).
|
|
321
|
+
*
|
|
322
|
+
* This was declared target-first while the runtime has always been
|
|
323
|
+
* event-first, so the two disagreed in the worst possible direction: `_target`
|
|
324
|
+
* accepted `string`, so the CORRECT call got as far as reading 'keydown' as a
|
|
325
|
+
* selector before failing on the handler, and an author who trusted the type
|
|
326
|
+
* and added a target wrote `useEventListener(window, 'keydown', fn)` — which
|
|
327
|
+
* binds an event named "window" with the string 'keydown' as its handler, and
|
|
328
|
+
* silently attaches nothing.
|
|
329
|
+
*
|
|
330
|
+
* `browser-composables.ts` had it right all along; the package shipped two
|
|
331
|
+
* declarations of one name with different arities and the registry got the
|
|
332
|
+
* wrong one. Same shape as the `reactive` split: one name, two conventions, and
|
|
333
|
+
* no way for the call site to tell which it has.
|
|
334
|
+
*/
|
|
274
335
|
declare function useEventListener<K extends keyof WindowEventMap>(
|
|
275
|
-
_target: Window | Document | HTMLElement | string | null,
|
|
276
336
|
_event: K,
|
|
277
337
|
_handler: (event: WindowEventMap[K]) => void,
|
|
278
|
-
_options?:
|
|
338
|
+
_options?: StxEventListenerOptions,
|
|
279
339
|
): StxCleanup
|
|
280
340
|
declare function useEventListener(
|
|
281
|
-
_target: Window | Document | HTMLElement | string | null,
|
|
282
341
|
_event: string,
|
|
283
342
|
_handler: (event: Event) => void,
|
|
284
|
-
_options?:
|
|
343
|
+
_options?: StxEventListenerOptions,
|
|
285
344
|
): StxCleanup
|
|
286
345
|
declare function useScrollLock(
|
|
287
346
|
_target?: HTMLElement | null | { current?: HTMLElement | null, value?: HTMLElement | null } | (() => HTMLElement | null | undefined),
|
|
@@ -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> }[], redirect?: string }>;
|
|
23
|
+
export declare function hydrateTemplateStream(compiled: CompiledTemplate, requestContext?: Record<string, any>): Promise<{ html: string, boundaries?: { id: string, render: () => Promise<string> }[], redirect?: string, cookies?: string[] }>;
|
|
24
24
|
/**
|
|
25
25
|
* Hydrate a fragment (for SPA navigation).
|
|
26
26
|
* Same as hydrateTemplate but returns the fragment portion.
|
|
@@ -6,9 +6,9 @@ export async function hydrateTemplate(compiled, requestContext = {}) {
|
|
|
6
6
|
return (await runHydration(compiled, requestContext)).html;
|
|
7
7
|
}
|
|
8
8
|
export async function hydrateTemplateStream(compiled, requestContext = {}) {
|
|
9
|
-
const { html, context, redirect } = await runHydration(compiled, requestContext);
|
|
9
|
+
const { html, context, redirect, cookies } = await runHydration(compiled, requestContext);
|
|
10
10
|
if (redirect)
|
|
11
|
-
return { html, redirect };
|
|
11
|
+
return { html, redirect, cookies };
|
|
12
12
|
const { extractStreamBoundaries } = await import("./streaming.js");
|
|
13
13
|
let boundaries = extractStreamBoundaries(context);
|
|
14
14
|
const templates = compiled.streamTemplates;
|
|
@@ -22,7 +22,7 @@ export async function hydrateTemplateStream(compiled, requestContext = {}) {
|
|
|
22
22
|
})), tplIds = new Set(tplBoundaries.map((b) => b.id));
|
|
23
23
|
boundaries = [...(boundaries || []).filter((b) => !tplIds.has(b.id)), ...tplBoundaries];
|
|
24
24
|
}
|
|
25
|
-
return { html, boundaries: boundaries && boundaries.length > 0 ? boundaries : void 0 };
|
|
25
|
+
return { html, boundaries: boundaries && boundaries.length > 0 ? boundaries : void 0, cookies };
|
|
26
26
|
}
|
|
27
27
|
async function runHydration(compiled, requestContext) {
|
|
28
28
|
const context = {
|
|
@@ -45,9 +45,9 @@ async function runHydration(compiled, requestContext) {
|
|
|
45
45
|
cookies: requestContext.cookies ?? {}
|
|
46
46
|
});
|
|
47
47
|
if (action.redirect)
|
|
48
|
-
return { html: compiled.html, context, redirect: action.redirect };
|
|
48
|
+
return { html: compiled.html, context, redirect: action.redirect, cookies: action.cookies };
|
|
49
49
|
if (!hasPlaceholders(compiled.html))
|
|
50
|
-
return { html: compiled.html, context };
|
|
50
|
+
return { html: compiled.html, context, cookies: action.cookies };
|
|
51
51
|
const values = new Map;
|
|
52
52
|
for (const [token, info] of Object.entries(compiled.placeholders))
|
|
53
53
|
if (info.expression)
|
|
@@ -60,7 +60,7 @@ async function runHydration(compiled, requestContext) {
|
|
|
60
60
|
} catch {
|
|
61
61
|
values.set(token, "");
|
|
62
62
|
}
|
|
63
|
-
return { html: replacePlaceholders(compiled.html, values), context };
|
|
63
|
+
return { html: replacePlaceholders(compiled.html, values), context, cookies: action.cookies };
|
|
64
64
|
}
|
|
65
65
|
export async function hydrateFragment(compiled, requestContext = {}) {
|
|
66
66
|
if (!compiled.hasServerScripts && !hasPlaceholders(compiled.fragment))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-named colours, so a component can say what a colour is FOR.
|
|
3
|
+
*
|
|
4
|
+
* `@stacksjs/components` named palette shades directly — `bg-gray-100`,
|
|
5
|
+
* `text-gray-600`, `ring-indigo-600` — with a `dark:` variant beside each. That
|
|
6
|
+
* renders correctly and is impossible for a host app to redirect by meaning: you
|
|
7
|
+
* can remap `gray`, but you cannot say "every border in the library comes from
|
|
8
|
+
* `--stx-line`", because nothing distinguishes a gray that is a border from a
|
|
9
|
+
* gray that is muted text (stacksjs/stx#1930).
|
|
10
|
+
*
|
|
11
|
+
* These tokens are added to the BASE theme rather than shipped as an opt-in
|
|
12
|
+
* preset. That matters for one reason: a component that says `text-fg-muted`
|
|
13
|
+
* must resolve in every app, or adopting the component library would silently
|
|
14
|
+
* require a config change and render unstyled without it. Adding names is purely
|
|
15
|
+
* additive — no existing utility changes, and no opacity modifier on an existing
|
|
16
|
+
* palette colour turns into `color-mix()`.
|
|
17
|
+
*
|
|
18
|
+
* ## Where the values come from
|
|
19
|
+
*
|
|
20
|
+
* Each token names a shade in crosswind's own palette rather than carrying a hex
|
|
21
|
+
* value, and the light/dark pairs are the ones the component library already
|
|
22
|
+
* used most — measured, not invented:
|
|
23
|
+
*
|
|
24
|
+
* text-gray-900 dark:text-gray-100 ×70 -> text-fg
|
|
25
|
+
* text-gray-500 dark:text-gray-400 ×24 -> text-fg-soft
|
|
26
|
+
* border-gray-200 dark:border-gray-700 ×21 -> border-line
|
|
27
|
+
*
|
|
28
|
+
* So migrating a component to a token is appearance-preserving wherever its
|
|
29
|
+
* pairing was the dominant one, and the defaults stay in step with the palette
|
|
30
|
+
* because they are resolved from it rather than copied.
|
|
31
|
+
*
|
|
32
|
+
* ## How dark mode works
|
|
33
|
+
*
|
|
34
|
+
* Each token resolves to `var(--stx-<name>, <light value>)`, and
|
|
35
|
+
* {@link semanticTokenCSS} emits `:root` and `.dark` blocks that set the
|
|
36
|
+
* variable. So `text-fg-muted` is one class that is correct in both modes, and
|
|
37
|
+
* an app overrides a role by setting the variable — at runtime, with no rebuild.
|
|
38
|
+
*
|
|
39
|
+
* The fallback is deliberately the LIGHT value. If the variable block ever fails
|
|
40
|
+
* to reach a page, light mode is still correct and dark mode falls back to the
|
|
41
|
+
* light colour: degraded, but readable. The alternative — no fallback — would
|
|
42
|
+
* render the token as an invalid colour and take the whole component's styling
|
|
43
|
+
* with it.
|
|
44
|
+
*
|
|
45
|
+
* @module theme-tokens
|
|
46
|
+
*/
|
|
47
|
+
/** A role, and the palette shades that back it in each mode. */
|
|
48
|
+
export interface SemanticToken {
|
|
49
|
+
/** Shade reference, e.g. `gray-900`. Resolved against the live palette. */
|
|
50
|
+
light: string;
|
|
51
|
+
dark: string;
|
|
52
|
+
/** What the role means, for the generated docs and for anyone reading here. */
|
|
53
|
+
description: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The role vocabulary.
|
|
57
|
+
*
|
|
58
|
+
* Five text roles rather than the usual three because the library genuinely
|
|
59
|
+
* distinguishes five: 900, 700, 600, 500 and 400 all appear as body-ish text
|
|
60
|
+
* with different dark partners. Collapsing them here would have been a redesign
|
|
61
|
+
* rather than a migration.
|
|
62
|
+
*/
|
|
63
|
+
export declare const SEMANTIC_TOKENS: Record<string, SemanticToken>;
|
|
64
|
+
/** The CSS custom property that backs a role. */
|
|
65
|
+
export declare function tokenVariable(name: string): string;
|
|
66
|
+
type Palette = Record<string, unknown>;
|
|
67
|
+
/**
|
|
68
|
+
* The palette entries to merge into `theme.colors`.
|
|
69
|
+
*
|
|
70
|
+
* Every entry is `var(--stx-<name>, <light value>)`, so the name works as an
|
|
71
|
+
* ordinary utility (`bg-surface`, `text-fg-muted`, `ring-line-strong`) and an
|
|
72
|
+
* app can move it by setting the variable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function semanticColors(palette: Palette): Record<string, string>;
|
|
75
|
+
/**
|
|
76
|
+
* The `:root` / `.dark` block that gives each role its value per mode.
|
|
77
|
+
*
|
|
78
|
+
* Emitted ahead of the generated utilities so an app's own stylesheet — which
|
|
79
|
+
* comes after — can override any of it without a specificity fight.
|
|
80
|
+
*
|
|
81
|
+
* `.dark` matches the class strategy the rest of stx uses (`dark:` variants
|
|
82
|
+
* compile to `.dark .dark\:x`), so a token and a `dark:` utility on the same
|
|
83
|
+
* page agree about what dark mode is.
|
|
84
|
+
*/
|
|
85
|
+
export declare function semanticTokenCSS(palette: Palette): string;
|
|
86
|
+
/** Every variable a role token reads, for documentation and tooling. */
|
|
87
|
+
export declare function semanticTokenNames(): string[];
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const SEMANTIC_TOKENS = {
|
|
2
|
+
fg: { light: "gray-900", dark: "gray-100", description: "Primary text" },
|
|
3
|
+
"fg-strong": { light: "gray-700", dark: "gray-300", description: "Labels, secondary headings" },
|
|
4
|
+
"fg-muted": { light: "gray-600", dark: "gray-400", description: "Supporting text" },
|
|
5
|
+
"fg-soft": { light: "gray-500", dark: "gray-400", description: "De-emphasised text" },
|
|
6
|
+
"fg-subtle": { light: "gray-400", dark: "gray-500", description: "Placeholders, disabled text" },
|
|
7
|
+
surface: { light: "gray-50", dark: "gray-800", description: "Page and panel background" },
|
|
8
|
+
"surface-raised": { light: "gray-100", dark: "gray-700", description: "Cards, popovers" },
|
|
9
|
+
"surface-sunken": { light: "gray-200", dark: "gray-700", description: "Wells, track backgrounds" },
|
|
10
|
+
line: { light: "gray-200", dark: "gray-700", description: "Default border, divider" },
|
|
11
|
+
"line-strong": { light: "gray-300", dark: "gray-600", description: "Input border, focus ring track" },
|
|
12
|
+
accent: { light: "indigo-600", dark: "indigo-400", description: "Primary action, selected state" },
|
|
13
|
+
info: { light: "blue-600", dark: "blue-400", description: "Informational emphasis, links" },
|
|
14
|
+
danger: { light: "red-600", dark: "red-400", description: "Errors, destructive actions" },
|
|
15
|
+
success: { light: "green-600", dark: "green-400", description: "Confirmation" },
|
|
16
|
+
warning: { light: "yellow-500", dark: "yellow-400", description: "Caution" },
|
|
17
|
+
"accent-solid": { light: "indigo-600", dark: "indigo-500", description: "Primary button fill" },
|
|
18
|
+
"info-solid": { light: "blue-600", dark: "blue-500", description: "Informational fill" },
|
|
19
|
+
"danger-solid": { light: "red-600", dark: "red-500", description: "Destructive button fill" },
|
|
20
|
+
"success-solid": { light: "green-600", dark: "green-500", description: "Confirmation fill" }
|
|
21
|
+
};
|
|
22
|
+
export function tokenVariable(name) {
|
|
23
|
+
return `--stx-${name}`;
|
|
24
|
+
}
|
|
25
|
+
function resolveShade(palette, reference) {
|
|
26
|
+
const split = reference.lastIndexOf("-");
|
|
27
|
+
if (split === -1)
|
|
28
|
+
return typeof palette[reference] === "string" ? palette[reference] : void 0;
|
|
29
|
+
const family = palette[reference.slice(0, split)];
|
|
30
|
+
if (!family || typeof family !== "object")
|
|
31
|
+
return;
|
|
32
|
+
const value = family[reference.slice(split + 1)];
|
|
33
|
+
return typeof value === "string" ? value : void 0;
|
|
34
|
+
}
|
|
35
|
+
export function semanticColors(palette) {
|
|
36
|
+
const colors = {};
|
|
37
|
+
for (const [name, token] of Object.entries(SEMANTIC_TOKENS)) {
|
|
38
|
+
const light = resolveShade(palette, token.light);
|
|
39
|
+
if (light === void 0)
|
|
40
|
+
continue;
|
|
41
|
+
colors[name] = `var(${tokenVariable(name)}, ${light})`;
|
|
42
|
+
}
|
|
43
|
+
return colors;
|
|
44
|
+
}
|
|
45
|
+
export function semanticTokenCSS(palette) {
|
|
46
|
+
const light = [], dark = [];
|
|
47
|
+
for (const [name, token] of Object.entries(SEMANTIC_TOKENS)) {
|
|
48
|
+
const lightValue = resolveShade(palette, token.light), darkValue = resolveShade(palette, token.dark);
|
|
49
|
+
if (lightValue === void 0)
|
|
50
|
+
continue;
|
|
51
|
+
light.push(` ${tokenVariable(name)}: ${lightValue};`);
|
|
52
|
+
if (darkValue !== void 0)
|
|
53
|
+
dark.push(` ${tokenVariable(name)}: ${darkValue};`);
|
|
54
|
+
}
|
|
55
|
+
if (light.length === 0)
|
|
56
|
+
return "";
|
|
57
|
+
return [
|
|
58
|
+
"/* stx role tokens \u2014 override any of these to re-theme by meaning. */",
|
|
59
|
+
":root {",
|
|
60
|
+
...light,
|
|
61
|
+
"}",
|
|
62
|
+
".dark {",
|
|
63
|
+
...dark,
|
|
64
|
+
"}",
|
|
65
|
+
""
|
|
66
|
+
].join(`
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
export function semanticTokenNames() {
|
|
70
|
+
return Object.keys(SEMANTIC_TOKENS).map(tokenVariable);
|
|
71
|
+
}
|
package/dist/typecheck.d.ts
CHANGED
|
@@ -14,8 +14,13 @@ export type { ScriptBlock, ScriptKind } from './stx-virtual-ts';
|
|
|
14
14
|
* in a `<script server>` block both reported "Cannot redeclare block-scoped
|
|
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
|
+
*
|
|
18
|
+
* `originDir` is the directory the `.stx` file really lives in. The buffer is
|
|
19
|
+
* written into the state directory, and tsc resolves a relative specifier
|
|
20
|
+
* against the file containing it — so without this, `../target` was looked for
|
|
21
|
+
* next to `.stx/typecheck/` (#1928).
|
|
17
22
|
*/
|
|
18
|
-
export declare function buildVirtualSource(block: ScriptBlock, serverCode?: string): string;
|
|
23
|
+
export declare function buildVirtualSource(block: ScriptBlock, serverCode?: string, originDir?: string): string;
|
|
19
24
|
/** Virtual path for a block, stable and traceable back to its origin. */
|
|
20
25
|
export declare function virtualPathFor(filePath: string, kind: DiagnosticOrigin, index: number): string;
|
|
21
26
|
/** Map a virtual path back to the `.stx` file it came from. */
|