@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/expressions.d.ts
CHANGED
|
@@ -84,27 +84,6 @@ export declare function escapeHtml(unsafe: string): string;
|
|
|
84
84
|
* or has reactive attributes (@if, @for) with function call expressions
|
|
85
85
|
*/
|
|
86
86
|
export declare function usesSignalsInScript(template: string): boolean;
|
|
87
|
-
/**
|
|
88
|
-
* Interpolate server-side `{{ expr }}` / `{!! expr !!}` expressions inside a
|
|
89
|
-
* `<script>` body, using the same evaluator as the main template.
|
|
90
|
-
*
|
|
91
|
-
* This makes server data available to client scripts without manual data-island
|
|
92
|
-
* plumbing — write `const PRICE = {{ car.price }}` and get `const PRICE = 42`.
|
|
93
|
-
*
|
|
94
|
-
* JavaScript-aware rules (different from HTML `{{ }}`):
|
|
95
|
-
* - `{{ expr }}` → `JSON.stringify(value)`. Strings become valid JS string
|
|
96
|
-
* literals, numbers/booleans pass through, objects/arrays become JSON
|
|
97
|
-
* literals. Safe to drop into any JS expression position.
|
|
98
|
-
* - `{!! expr !!}` → raw `String(value)`. Use when you want the value spliced
|
|
99
|
-
* verbatim (e.g. you've already serialized it yourself).
|
|
100
|
-
* - Expressions that fail to evaluate (reference a client-only signal, a
|
|
101
|
-
* loop variable, or anything not in context) are left **untouched** so the
|
|
102
|
-
* client runtime can process them later. This mirrors the "preserve for
|
|
103
|
-
* client" semantics of the main expression processor when signals are in use.
|
|
104
|
-
*
|
|
105
|
-
* Only the content strictly between `<script>` and `</script>` is touched —
|
|
106
|
-
* attributes and surrounding HTML are left alone.
|
|
107
|
-
*/
|
|
108
87
|
export declare function interpolateScriptExpressions(scriptBody: string, context: Record<string, any>): string;
|
|
109
88
|
/**
|
|
110
89
|
* Interpolate expressions in a `<script>` tag's ATTRIBUTES.
|
package/dist/expressions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripCommentsAndLiterals } from "./strip-literals.js";
|
|
1
2
|
import { createSafeFunction, isExpressionSafe, safeEvaluate } from "./safe-evaluator.js";
|
|
2
3
|
import { createDetailedErrorMessage } from "./utils.js";
|
|
3
4
|
import { createPlaceholder } from "./placeholder.js";
|
|
@@ -298,6 +299,16 @@ function expressionUsesOnlyContextVars(expr, context) {
|
|
|
298
299
|
}
|
|
299
300
|
return !0;
|
|
300
301
|
}
|
|
302
|
+
function expressionReadsServerBinding(expr, context) {
|
|
303
|
+
const declared = context.__stx_serverDeclared;
|
|
304
|
+
if (!declared || declared.size === 0)
|
|
305
|
+
return !1;
|
|
306
|
+
const searchable = stripCommentsAndLiterals(expr);
|
|
307
|
+
for (const name of declared)
|
|
308
|
+
if (new RegExp(`(?<![.#\\w$])${name}(?![\\w$])`).test(searchable))
|
|
309
|
+
return !0;
|
|
310
|
+
return !1;
|
|
311
|
+
}
|
|
301
312
|
export function interpolateScriptExpressions(scriptBody, context) {
|
|
302
313
|
let output = scriptBody;
|
|
303
314
|
output = output.replace(/\{!!([\s\S]*?)!!\}/g, (match, expr) => {
|
|
@@ -426,7 +437,7 @@ export function processExpressions(template, context, filePath) {
|
|
|
426
437
|
return match;
|
|
427
438
|
if (hasSignals && !expressionUsesOnlyContextVars(trimmedExpr, context))
|
|
428
439
|
return match;
|
|
429
|
-
if (
|
|
440
|
+
if (context.__stx_buildMode === "compile" && !hasSignals && (value === void 0 || expressionReadsServerBinding(trimmedExpr, context)))
|
|
430
441
|
return createPlaceholder("expr", trimmedExpr);
|
|
431
442
|
return value !== void 0 && value !== null ? escapeHtml(String(value)) : "";
|
|
432
443
|
} catch (error) {
|
package/dist/find-body-tag.js
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
const SKIP_REGIONS = [
|
|
2
|
-
{ open:
|
|
3
|
-
{ open: /<script\b/
|
|
4
|
-
{ open: /<style\b/
|
|
5
|
-
];
|
|
2
|
+
{ open: /<!--/g, close: /-->/g },
|
|
3
|
+
{ open: /<script\b/gi, close: /<\/script\s*>/gi },
|
|
4
|
+
{ open: /<style\b/gi, close: /<\/style\s*>/gi }
|
|
5
|
+
], BODY_OPEN = /<body\b([^>]*)>/gi;
|
|
6
6
|
export function findBodyOpenTag(html) {
|
|
7
|
-
let cursor = 0;
|
|
7
|
+
let cursor = 0, bodyMatch;
|
|
8
|
+
const opens = SKIP_REGIONS.map(() => {
|
|
9
|
+
return;
|
|
10
|
+
});
|
|
8
11
|
while (cursor < html.length) {
|
|
9
|
-
|
|
12
|
+
if (bodyMatch === void 0 || bodyMatch !== null && bodyMatch.index < cursor) {
|
|
13
|
+
BODY_OPEN.lastIndex = cursor;
|
|
14
|
+
bodyMatch = BODY_OPEN.exec(html);
|
|
15
|
+
}
|
|
10
16
|
if (!bodyMatch)
|
|
11
17
|
return null;
|
|
12
|
-
const bodyAt =
|
|
13
|
-
let nearestSkipAt = -1, nearestClose =
|
|
14
|
-
for (
|
|
15
|
-
const
|
|
18
|
+
const bodyAt = bodyMatch.index;
|
|
19
|
+
let nearestSkipAt = -1, nearestClose = null;
|
|
20
|
+
for (let i = 0;i < SKIP_REGIONS.length; i++) {
|
|
21
|
+
const region = SKIP_REGIONS[i];
|
|
22
|
+
let found = opens[i];
|
|
23
|
+
if (found === void 0 || found !== null && found.index < cursor) {
|
|
24
|
+
region.open.lastIndex = cursor;
|
|
25
|
+
found = region.open.exec(html);
|
|
26
|
+
opens[i] = found;
|
|
27
|
+
}
|
|
16
28
|
if (!found)
|
|
17
29
|
continue;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
nearestSkipAt = at;
|
|
30
|
+
if (found.index < bodyAt && (nearestSkipAt === -1 || found.index < nearestSkipAt)) {
|
|
31
|
+
nearestSkipAt = found.index;
|
|
21
32
|
nearestClose = region.close;
|
|
22
33
|
}
|
|
23
34
|
}
|
|
24
|
-
if (nearestSkipAt === -1)
|
|
35
|
+
if (nearestSkipAt === -1 || !nearestClose)
|
|
25
36
|
return {
|
|
26
37
|
index: bodyAt,
|
|
27
38
|
tag: bodyMatch[0],
|
|
28
39
|
attrs: bodyMatch[1] ?? ""
|
|
29
40
|
};
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
nearestClose.lastIndex = nearestSkipAt;
|
|
42
|
+
const closed = nearestClose.exec(html);
|
|
43
|
+
if (!closed)
|
|
32
44
|
return null;
|
|
33
|
-
cursor =
|
|
45
|
+
cursor = closed.index + closed[0].length;
|
|
34
46
|
}
|
|
35
47
|
return null;
|
|
36
48
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,12 @@ export { type AnalysisResult } from './analyzer';
|
|
|
85
85
|
export { type AnalyzeOptions, type ChunkInfo } from './production-build';
|
|
86
86
|
// generateReport moved to stx/visual-testing
|
|
87
87
|
export { fileExists } from './utils';
|
|
88
|
+
// `CompressionOptions` is declared twice — the response compressor's options
|
|
89
|
+
// (`./compression`, what a server tunes) and the production build's
|
|
90
|
+
// (`./production-build`). A wildcard collision exports NEITHER, so the
|
|
91
|
+
// user-facing one is named explicitly here; the build's is reachable from the
|
|
92
|
+
// `stx/production-build` subpath.
|
|
93
|
+
export { type CompressionOptions } from './compression';
|
|
88
94
|
export { type StreamingConfig } from './types';
|
|
89
95
|
export { type Middleware } from './ssr';
|
|
90
96
|
// Re-export the Icon-collection preloader so dev servers (e.g.
|
|
@@ -160,6 +166,7 @@ export * from './keep-alive';
|
|
|
160
166
|
export * from './loops';
|
|
161
167
|
export * from './markdown';
|
|
162
168
|
export * from './middleware';
|
|
169
|
+
export * from './page-action';
|
|
163
170
|
export * from './parser/index';
|
|
164
171
|
export * from './manifest';
|
|
165
172
|
export * from './placeholder';
|
|
@@ -196,6 +203,7 @@ export * from './loading-indicator';
|
|
|
196
203
|
export * from './reactive-bindings';
|
|
197
204
|
export * from './client-helpers';
|
|
198
205
|
export * from './client-script';
|
|
206
|
+
export * from './strip-literals';
|
|
199
207
|
export * from './component-hmr';
|
|
200
208
|
export * from './hydration';
|
|
201
209
|
// ssg moved to stx/ssg
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ export * from "./keep-alive.js";
|
|
|
79
79
|
export * from "./loops.js";
|
|
80
80
|
export * from "./markdown.js";
|
|
81
81
|
export * from "./middleware.js";
|
|
82
|
+
export * from "./page-action.js";
|
|
82
83
|
export * from "./parser/index.js";
|
|
83
84
|
export * from "./manifest.js";
|
|
84
85
|
export * from "./placeholder.js";
|
|
@@ -111,6 +112,7 @@ export * from "./loading-indicator.js";
|
|
|
111
112
|
export * from "./reactive-bindings.js";
|
|
112
113
|
export * from "./client-helpers.js";
|
|
113
114
|
export * from "./client-script.js";
|
|
115
|
+
export * from "./strip-literals.js";
|
|
114
116
|
export * from "./component-hmr.js";
|
|
115
117
|
export * from "./hydration.js";
|
|
116
118
|
export * from "./ssr.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a form submission body into a plain object.
|
|
3
|
+
*
|
|
4
|
+
* Handles both encodings a browser form can send: `multipart/form-data` and
|
|
5
|
+
* `application/x-www-form-urlencoded`. A repeated field name collapses to an
|
|
6
|
+
* array, so `<input name="tag">` twice reads as `['a', 'b']` rather than
|
|
7
|
+
* silently keeping only the last one — checkbox groups are the common case.
|
|
8
|
+
*
|
|
9
|
+
* File parts are skipped: the value would be a `File`, and a page action that
|
|
10
|
+
* wants uploads should read `request.formData()` itself rather than have this
|
|
11
|
+
* hand it something that cannot round-trip into a re-rendered form field.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseFormBody(request: Request): Promise<Record<string, string | string[]>>;
|
|
14
|
+
/**
|
|
15
|
+
* Whether this request should run a page action at all.
|
|
16
|
+
*
|
|
17
|
+
* GET and HEAD never do — a page action is for submissions, and running one on
|
|
18
|
+
* a GET would make every page load a mutation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isActionableMethod(method: string | undefined): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Run a page's own `action`, if it declared one, and merge what it returned.
|
|
23
|
+
*
|
|
24
|
+
* The action is picked up off the render context rather than parsed out of the
|
|
25
|
+
* source: the server block has already run by this point, so a declared
|
|
26
|
+
* `action` is simply there — and it can close over anything else the block
|
|
27
|
+
* defined, which a regex-extracted function could not.
|
|
28
|
+
*
|
|
29
|
+
* Inert on GET and for any page without an action, so a page that does not opt
|
|
30
|
+
* in pays one `typeof` check.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runPageAction(context: Record<string, any>, options?: RunPageActionOptions): Promise<PageActionResult>;
|
|
33
|
+
/**
|
|
34
|
+
* The response a redirecting action should produce.
|
|
35
|
+
*
|
|
36
|
+
* 303 rather than 302 deliberately: it is the status that makes a browser
|
|
37
|
+
* follow up with a GET, so a reload or a Back does not resubmit the form.
|
|
38
|
+
*/
|
|
39
|
+
export declare function actionRedirectResponse(to: string): Response;
|
|
40
|
+
/**
|
|
41
|
+
* A page handling its own form submission (stacksjs/stx#1847).
|
|
42
|
+
*
|
|
43
|
+
* A page that declares `action` in its `<script server>` block receives its own
|
|
44
|
+
* non-GET request. Whatever the action returns is merged into the render
|
|
45
|
+
* context, so `errors` and `values` repopulate the very same template that drew
|
|
46
|
+
* the form — one handler for the no-JS submit and the enhanced one, which is
|
|
47
|
+
* the half of the Laravel/Remix/SvelteKit contract stx was missing.
|
|
48
|
+
*
|
|
49
|
+
* ## Why this is a module rather than two implementations
|
|
50
|
+
*
|
|
51
|
+
* It shipped in `bun-plugin/src/serve.ts` only, so a form worked under
|
|
52
|
+
* `buddy dev` and silently did nothing in production — the request fell through
|
|
53
|
+
* to the ordinary render and answered 200 with the pre-submit markup, which is
|
|
54
|
+
* indistinguishable from the action having run and found nothing wrong. That is
|
|
55
|
+
* the same dev-only-correctness shape as `stx typecheck` being stricter than
|
|
56
|
+
* the editor, and this repo has been bitten repeatedly by two hand-maintained
|
|
57
|
+
* copies of one rule drifting apart. One module, both servers.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Everything an action is given about the request.
|
|
61
|
+
*
|
|
62
|
+
* A plain object rather than the `Request` alone, so an action is a pure
|
|
63
|
+
* function of its inputs and unit-tests without a server around it.
|
|
64
|
+
*/
|
|
65
|
+
export declare interface PageActionContext {
|
|
66
|
+
request: Request
|
|
67
|
+
form: Record<string, string | string[]>
|
|
68
|
+
params: Record<string, string>
|
|
69
|
+
query: Record<string, unknown>
|
|
70
|
+
cookies: Record<string, string>
|
|
71
|
+
}
|
|
72
|
+
/** What running an action produced. */
|
|
73
|
+
export declare interface PageActionResult {
|
|
74
|
+
redirect?: string
|
|
75
|
+
ran: boolean
|
|
76
|
+
}
|
|
77
|
+
/** Request data a caller has already parsed, if it has. */
|
|
78
|
+
export declare interface RunPageActionOptions {
|
|
79
|
+
request?: Request
|
|
80
|
+
method?: string
|
|
81
|
+
params?: Record<string, string>
|
|
82
|
+
cookies?: Record<string, string>
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export async function parseFormBody(request) {
|
|
2
|
+
const type = request.headers.get("content-type") || "", form = {}, put = (key, value) => {
|
|
3
|
+
const existing = form[key];
|
|
4
|
+
if (existing === void 0)
|
|
5
|
+
form[key] = value;
|
|
6
|
+
else if (Array.isArray(existing))
|
|
7
|
+
existing.push(value);
|
|
8
|
+
else
|
|
9
|
+
form[key] = [existing, value];
|
|
10
|
+
};
|
|
11
|
+
if (type.includes("multipart/form-data")) {
|
|
12
|
+
const data = await request.formData();
|
|
13
|
+
for (const [key, value] of data.entries())
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
put(key, value);
|
|
16
|
+
return form;
|
|
17
|
+
}
|
|
18
|
+
if (type.includes("application/x-www-form-urlencoded")) {
|
|
19
|
+
for (const [key, value] of new URLSearchParams(await request.text()).entries())
|
|
20
|
+
put(key, value);
|
|
21
|
+
return form;
|
|
22
|
+
}
|
|
23
|
+
return form;
|
|
24
|
+
}
|
|
25
|
+
export function isActionableMethod(method) {
|
|
26
|
+
if (!method)
|
|
27
|
+
return !1;
|
|
28
|
+
const verb = method.toUpperCase();
|
|
29
|
+
return verb !== "GET" && verb !== "HEAD";
|
|
30
|
+
}
|
|
31
|
+
export async function runPageAction(context, options = {}) {
|
|
32
|
+
const request = options.request, method = options.method ?? request?.method;
|
|
33
|
+
if (!request || !isActionableMethod(method))
|
|
34
|
+
return { ran: !1 };
|
|
35
|
+
if (typeof context.action !== "function")
|
|
36
|
+
return { ran: !1 };
|
|
37
|
+
let form = {};
|
|
38
|
+
try {
|
|
39
|
+
form = await parseFormBody(request);
|
|
40
|
+
} catch {}
|
|
41
|
+
const result = await context.action({
|
|
42
|
+
request,
|
|
43
|
+
form,
|
|
44
|
+
params: options.params ?? {},
|
|
45
|
+
query: context.query ?? {},
|
|
46
|
+
cookies: options.cookies ?? {}
|
|
47
|
+
});
|
|
48
|
+
if (result && typeof result === "object") {
|
|
49
|
+
const to = result.redirect;
|
|
50
|
+
if (typeof to === "string" && to)
|
|
51
|
+
return { redirect: to, ran: !0 };
|
|
52
|
+
Object.assign(context, result);
|
|
53
|
+
}
|
|
54
|
+
return { ran: !0 };
|
|
55
|
+
}
|
|
56
|
+
export function actionRedirectResponse(to) {
|
|
57
|
+
return new Response(null, {
|
|
58
|
+
status: 303,
|
|
59
|
+
headers: {
|
|
60
|
+
Location: to,
|
|
61
|
+
"Cache-Control": "no-store"
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -21,6 +21,7 @@ function copyPublicAssets(srcDir, destDir) {
|
|
|
21
21
|
let copied = 0, bytes = 0;
|
|
22
22
|
if (!fs.existsSync(srcDir))
|
|
23
23
|
return { copied, bytes };
|
|
24
|
+
fs.mkdirSync(destDir, { recursive: !0 });
|
|
24
25
|
function walk(currentSrc, currentDest) {
|
|
25
26
|
const entries = fs.readdirSync(currentSrc, { withFileTypes: !0 });
|
|
26
27
|
for (const entry of entries) {
|
|
@@ -7,6 +7,7 @@ import { extractLayoutMetadata } from "./app-shell.js";
|
|
|
7
7
|
import { pageShipsSignalsRuntime } from "./runtime-injection.js";
|
|
8
8
|
import { patternToRegex } from "stx-router";
|
|
9
9
|
import { compressResponse } from "./compression.js";
|
|
10
|
+
import { actionRedirectResponse, isActionableMethod } from "./page-action.js";
|
|
10
11
|
const MIME_TYPES = {
|
|
11
12
|
".js": "application/javascript",
|
|
12
13
|
".mjs": "application/javascript",
|
|
@@ -143,7 +144,7 @@ export async function startProductionServer(options = {}) {
|
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
|
-
const compiled = compiledTemplates.get(matchedRoute.pattern);
|
|
147
|
+
const isMutating = isActionableMethod(request.method), compiled = compiledTemplates.get(matchedRoute.pattern);
|
|
147
148
|
if (!compiled) {
|
|
148
149
|
const errorPage = exactRoutes.get("/500") || null;
|
|
149
150
|
if (errorPage) {
|
|
@@ -156,7 +157,7 @@ export async function startProductionServer(options = {}) {
|
|
|
156
157
|
}
|
|
157
158
|
return new Response("Internal Server Error", { status: 500 });
|
|
158
159
|
}
|
|
159
|
-
if (!compiled.hasServerScripts && Object.keys(compiled.placeholders).length === 0)
|
|
160
|
+
if (!isMutating && !compiled.hasServerScripts && Object.keys(compiled.placeholders).length === 0)
|
|
160
161
|
return new Response(compiled.html, {
|
|
161
162
|
headers: {
|
|
162
163
|
"Content-Type": "text/html",
|
|
@@ -164,7 +165,13 @@ export async function startProductionServer(options = {}) {
|
|
|
164
165
|
}
|
|
165
166
|
});
|
|
166
167
|
try {
|
|
167
|
-
const { html, boundaries } = await hydrateTemplateStream(compiled, {
|
|
168
|
+
const { html, boundaries, redirect } = await hydrateTemplateStream(compiled, {
|
|
169
|
+
params,
|
|
170
|
+
request,
|
|
171
|
+
method: request.method
|
|
172
|
+
});
|
|
173
|
+
if (redirect)
|
|
174
|
+
return actionRedirectResponse(redirect);
|
|
168
175
|
if (boundaries && boundaries.length > 0) {
|
|
169
176
|
const { renderStreamingPage, streamToResponse } = await import("./streaming.js");
|
|
170
177
|
return streamToResponse(renderStreamingPage(html, boundaries, { timeoutMs: 30000 }), {
|