@stacksjs/stx 0.2.152 → 0.2.154
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/animation.js +6 -6
- package/dist/app-shell.js +2 -1
- package/dist/browser-composables.d.ts +8 -0
- package/dist/build-id.d.ts +49 -0
- package/dist/build-id.js +20 -0
- package/dist/build-message.d.ts +10 -0
- package/dist/build-message.js +26 -0
- package/dist/builtins/attrs.js +6 -1
- package/dist/cli.js +1743 -1255
- package/dist/client-script-bundler.js +2 -4
- package/dist/client-script.d.ts +29 -0
- package/dist/client-script.js +142 -97
- package/dist/color-mode-boot.d.ts +46 -10
- package/dist/color-mode-boot.js +53 -13
- package/dist/component-client-factories.js +3 -2
- package/dist/component-renderer.js +10 -21
- package/dist/composables/index.d.ts +8 -1
- package/dist/composables/use-clipboard.d.ts +1 -5
- package/dist/composables/use-clipboard.js +57 -46
- package/dist/composables/use-color-mode.d.ts +19 -4
- package/dist/composables/use-color-mode.js +49 -28
- package/dist/composables/use-router.d.ts +28 -11
- package/dist/composables/use-router.js +63 -9
- package/dist/composables/use-storage.d.ts +14 -15
- package/dist/composables/use-storage.js +63 -2
- package/dist/config.js +22 -7
- package/dist/craft.js +1319 -841
- package/dist/dev-server/crosswind.js +8 -6
- package/dist/dev-server/serve-app.js +13 -7
- package/dist/dev-server/serve-markdown.js +4 -1
- package/dist/dev-server/serve-multi.js +32 -20
- package/dist/dev-server/types.d.ts +8 -1
- package/dist/document-shell.d.ts +120 -0
- package/dist/document-shell.js +161 -14
- package/dist/dynamic-components.js +123 -4
- package/dist/events.d.ts +26 -49
- package/dist/events.js +23 -9
- package/dist/expressions.d.ts +1 -1
- package/dist/expressions.js +3 -3
- package/dist/framework-composables.d.ts +41 -0
- package/dist/framework-composables.js +115 -0
- package/dist/head.js +5 -1
- package/dist/hot-reload.js +8 -2
- package/dist/includes.js +4 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/isolated-build.d.ts +12 -0
- package/dist/isolated-build.js +18 -0
- package/dist/plugin-system.d.ts +1 -1
- package/dist/process.js +46 -12
- package/dist/production-builder.js +3 -1
- package/dist/production-server.js +4 -1
- package/dist/prop-sentinels.d.ts +31 -0
- package/dist/prop-sentinels.js +7 -0
- package/dist/pwa.js +1258 -780
- package/dist/reactive.js +1 -1
- package/dist/reactivity.d.ts +0 -15
- package/dist/reactivity.js +15 -6
- package/dist/runtime-globals.d.ts +40 -2
- package/dist/runtime-globals.js +90 -0
- package/dist/runtime-injection.d.ts +51 -0
- package/dist/runtime-injection.js +13 -0
- package/dist/script-emit.d.ts +20 -0
- package/dist/script-emit.js +11 -0
- package/dist/script-validation.js +12 -7
- package/dist/signal-processing.js +25 -40
- package/dist/signals-api.d.ts +30 -0
- package/dist/signals-api.js +18 -0
- package/dist/signals.js +532 -85
- package/dist/site-builder/build.js +21 -7
- package/dist/site-builder/seo.js +19 -10
- package/dist/site-builder/theme.js +5 -4
- package/dist/site-builder/types.d.ts +1 -0
- package/dist/ssg.js +1255 -777
- package/dist/state-management.js +4 -1
- package/dist/store-loader.d.ts +0 -11
- package/dist/store-loader.js +27 -3
- package/dist/strict-lint.d.ts +9 -6
- package/dist/strict-lint.js +2 -21
- package/dist/stx.d.ts +125 -22
- package/dist/types/config-types.d.ts +1 -0
- package/dist/unresolved-identifiers.d.ts +50 -0
- package/dist/unresolved-identifiers.js +146 -0
- package/dist/utils.js +3 -3
- package/dist/variable-extractor.js +17 -1
- package/dist/x-element.js +1 -1
- package/package.json +5 -5
- package/stx.d.ts +125 -22
|
@@ -3,6 +3,115 @@ import { safeEvaluate } from "./safe-evaluator.js";
|
|
|
3
3
|
function escapeHtmlComment(value) {
|
|
4
4
|
return value.replace(/--/g, "- -").replace(/</g, "<").replace(/>/g, ">");
|
|
5
5
|
}
|
|
6
|
+
const HTML_TAGS = new Set([
|
|
7
|
+
"a",
|
|
8
|
+
"abbr",
|
|
9
|
+
"address",
|
|
10
|
+
"article",
|
|
11
|
+
"aside",
|
|
12
|
+
"audio",
|
|
13
|
+
"b",
|
|
14
|
+
"bdi",
|
|
15
|
+
"bdo",
|
|
16
|
+
"blockquote",
|
|
17
|
+
"button",
|
|
18
|
+
"canvas",
|
|
19
|
+
"caption",
|
|
20
|
+
"cite",
|
|
21
|
+
"code",
|
|
22
|
+
"col",
|
|
23
|
+
"colgroup",
|
|
24
|
+
"data",
|
|
25
|
+
"datalist",
|
|
26
|
+
"dd",
|
|
27
|
+
"del",
|
|
28
|
+
"details",
|
|
29
|
+
"dfn",
|
|
30
|
+
"dialog",
|
|
31
|
+
"div",
|
|
32
|
+
"dl",
|
|
33
|
+
"dt",
|
|
34
|
+
"em",
|
|
35
|
+
"fieldset",
|
|
36
|
+
"figcaption",
|
|
37
|
+
"figure",
|
|
38
|
+
"footer",
|
|
39
|
+
"form",
|
|
40
|
+
"h1",
|
|
41
|
+
"h2",
|
|
42
|
+
"h3",
|
|
43
|
+
"h4",
|
|
44
|
+
"h5",
|
|
45
|
+
"h6",
|
|
46
|
+
"header",
|
|
47
|
+
"hgroup",
|
|
48
|
+
"hr",
|
|
49
|
+
"i",
|
|
50
|
+
"iframe",
|
|
51
|
+
"img",
|
|
52
|
+
"input",
|
|
53
|
+
"ins",
|
|
54
|
+
"kbd",
|
|
55
|
+
"label",
|
|
56
|
+
"legend",
|
|
57
|
+
"li",
|
|
58
|
+
"main",
|
|
59
|
+
"mark",
|
|
60
|
+
"menu",
|
|
61
|
+
"meter",
|
|
62
|
+
"nav",
|
|
63
|
+
"ol",
|
|
64
|
+
"optgroup",
|
|
65
|
+
"option",
|
|
66
|
+
"output",
|
|
67
|
+
"p",
|
|
68
|
+
"picture",
|
|
69
|
+
"pre",
|
|
70
|
+
"progress",
|
|
71
|
+
"q",
|
|
72
|
+
"rp",
|
|
73
|
+
"rt",
|
|
74
|
+
"ruby",
|
|
75
|
+
"s",
|
|
76
|
+
"samp",
|
|
77
|
+
"section",
|
|
78
|
+
"select",
|
|
79
|
+
"small",
|
|
80
|
+
"span",
|
|
81
|
+
"strong",
|
|
82
|
+
"sub",
|
|
83
|
+
"summary",
|
|
84
|
+
"sup",
|
|
85
|
+
"table",
|
|
86
|
+
"tbody",
|
|
87
|
+
"td",
|
|
88
|
+
"textarea",
|
|
89
|
+
"tfoot",
|
|
90
|
+
"th",
|
|
91
|
+
"thead",
|
|
92
|
+
"time",
|
|
93
|
+
"tr",
|
|
94
|
+
"u",
|
|
95
|
+
"ul",
|
|
96
|
+
"video"
|
|
97
|
+
]), VOID_TAGS = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
|
|
98
|
+
function resolveIsName(expr, context) {
|
|
99
|
+
const mustache = /^\s*\{\{\s*([\s\S]+?)\s*\}\}\s*$/.exec(expr), source = mustache ? mustache[1] : expr, evaluated = safeEvaluate(source, context);
|
|
100
|
+
if (evaluated && typeof evaluated === "string")
|
|
101
|
+
return evaluated;
|
|
102
|
+
const literal = source.trim();
|
|
103
|
+
return HTML_TAGS.has(literal) ? literal : null;
|
|
104
|
+
}
|
|
105
|
+
function displayExpression(expr) {
|
|
106
|
+
const mustache = /^\s*\{\{\s*([\s\S]+?)\s*\}\}\s*$/.exec(expr);
|
|
107
|
+
return mustache ? mustache[1] : expr;
|
|
108
|
+
}
|
|
109
|
+
function renderAsElement(name, attrs, slotContent) {
|
|
110
|
+
const cleanedAttrs = attrs.replace(/\s*(?::is|v-bind:is)\s*=\s*"[^"]*"/gi, "").trim(), open = cleanedAttrs ? `<${name} ${cleanedAttrs}>` : `<${name}>`;
|
|
111
|
+
if (VOID_TAGS.has(name))
|
|
112
|
+
return cleanedAttrs ? `<${name} ${cleanedAttrs}>` : `<${name}>`;
|
|
113
|
+
return `${open}${slotContent}</${name}>`;
|
|
114
|
+
}
|
|
6
115
|
export async function processDynamicComponents(template, context, filePath, options, dependencies) {
|
|
7
116
|
let result = template;
|
|
8
117
|
const componentPattern = /<component\s+(?::is|v-bind:is)\s*=\s*"([^"]*)"([^>]*)>([\s\S]*?)<\/component>/gi, matches = [];
|
|
@@ -18,12 +127,17 @@ export async function processDynamicComponents(template, context, filePath, opti
|
|
|
18
127
|
for (let i = matches.length - 1;i >= 0; i--) {
|
|
19
128
|
const m = matches[i];
|
|
20
129
|
try {
|
|
21
|
-
const componentName =
|
|
130
|
+
const componentName = resolveIsName(m.expr, context);
|
|
22
131
|
if (!componentName || typeof componentName !== "string") {
|
|
23
|
-
const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(m.expr)}" -->`;
|
|
132
|
+
const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(displayExpression(m.expr))}" -->`;
|
|
24
133
|
result = result.slice(0, m.index) + replacement + result.slice(m.index + m.full.length);
|
|
25
134
|
continue;
|
|
26
135
|
}
|
|
136
|
+
if (HTML_TAGS.has(componentName)) {
|
|
137
|
+
const rendered = renderAsElement(componentName, m.attrs, m.slotContent);
|
|
138
|
+
result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
27
141
|
const propsStr = m.attrs.trim(), props = {}, propsPattern = /(?::([a-zA-Z_][a-zA-Z0-9_-]*)|([a-zA-Z_][a-zA-Z0-9_-]*))\s*=\s*"([^"]*)"/g;
|
|
28
142
|
let propMatch;
|
|
29
143
|
while ((propMatch = propsPattern.exec(propsStr)) !== null) {
|
|
@@ -61,12 +175,17 @@ async function processSelfClosingDynamicComponents(template, context, filePath,
|
|
|
61
175
|
for (let i = matches.length - 1;i >= 0; i--) {
|
|
62
176
|
const m = matches[i];
|
|
63
177
|
try {
|
|
64
|
-
const componentName =
|
|
178
|
+
const componentName = resolveIsName(m.expr, context);
|
|
65
179
|
if (!componentName || typeof componentName !== "string") {
|
|
66
|
-
const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(m.expr)}" -->`;
|
|
180
|
+
const replacement = `<!-- dynamic component: could not resolve "${escapeHtmlComment(displayExpression(m.expr))}" -->`;
|
|
67
181
|
result = result.slice(0, m.index) + replacement + result.slice(m.index + m.full.length);
|
|
68
182
|
continue;
|
|
69
183
|
}
|
|
184
|
+
if (HTML_TAGS.has(componentName)) {
|
|
185
|
+
const rendered = renderAsElement(componentName, m.attrs, "");
|
|
186
|
+
result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
70
189
|
const componentsDir = options.componentsDir || "components", rendered = await renderComponentWithSlot(componentName, {}, "", componentsDir, context, filePath, options, new Set, dependencies ?? new Set);
|
|
71
190
|
result = result.slice(0, m.index) + rendered + result.slice(m.index + m.full.length);
|
|
72
191
|
} catch (error) {
|
package/dist/events.d.ts
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@click="doThing"` names a function, so it must be INVOKED — not evaluated.
|
|
3
|
+
*
|
|
4
|
+
* The declarative runtime learned this in #1695: without it the expression falls
|
|
5
|
+
* through to a generic `new Function` and runs as a discarded identifier
|
|
6
|
+
* statement, so the click silently does nothing. Every IMPERATIVE binding path
|
|
7
|
+
* still evaluated it, in three separate places — the inline `else` branch
|
|
8
|
+
* emitted by client-script.ts, the `__stx_runHandler` fallback below, and
|
|
9
|
+
* `__stx_execute` in the x-data bridge — so the declarative path disagreed with
|
|
10
|
+
* all three (#1824).
|
|
11
|
+
*
|
|
12
|
+
* Normalising here, at emit time, rather than inside each of those runtimes:
|
|
13
|
+
* the handler text is already known when it is emitted, so one transform covers
|
|
14
|
+
* every path, and the rule stays out of the generated template literals.
|
|
15
|
+
*
|
|
16
|
+
* Restricted to a plain identifier, matching the runtime's own check exactly —
|
|
17
|
+
* a dotted path would require `obj` to exist before `typeof obj.fn` could be
|
|
18
|
+
* asked. The `_isSignal` exclusion is the runtime's too, and it matters because
|
|
19
|
+
* signals ARE functions: without it `@click="count"` would read the signal
|
|
20
|
+
* instead of doing nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function asInvocableStatement(handler: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Generate the complete runtime script
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateRuntimeScript(events: ParsedEvent[]): string;
|
|
1
27
|
/**
|
|
2
28
|
* Extract and process event attributes from a template without generating
|
|
3
29
|
* a standalone script. Returns the modified template (with @event attributes
|
|
@@ -33,55 +59,6 @@ export declare function processEventDirectives(template: string, context: Record
|
|
|
33
59
|
* Check if a template contains event directives
|
|
34
60
|
*/
|
|
35
61
|
export declare function hasEventDirectives(template: string): boolean;
|
|
36
|
-
/* eslint-disable prefer-const, style/max-statements-per-line, no-super-linear-backtracking, regexp/no-unused-capturing-group */
|
|
37
|
-
/**
|
|
38
|
-
* Event Directives Module
|
|
39
|
-
*
|
|
40
|
-
* Provides Alpine.js-style event handling for STX templates.
|
|
41
|
-
* Transforms `@click`, `@keydown.enter`, etc. into client-side event listeners.
|
|
42
|
-
*
|
|
43
|
-
* ## Supported Syntax
|
|
44
|
-
*
|
|
45
|
-
* ### Basic Events
|
|
46
|
-
* ```html
|
|
47
|
-
* <button @click="count++">Increment</button>
|
|
48
|
-
* <button @click="handleClick()">Click me</button>
|
|
49
|
-
* <form @submit="handleSubmit()">...</form>
|
|
50
|
-
* ```
|
|
51
|
-
*
|
|
52
|
-
* ### Event Modifiers
|
|
53
|
-
* ```html
|
|
54
|
-
* <form @submit.prevent="handleSubmit()">...</form>
|
|
55
|
-
* <a @click.prevent.stop="navigate()">Link</a>
|
|
56
|
-
* <button @click.once="initialize()">Init</button>
|
|
57
|
-
* <div @click.self="onlyThis()">...</div>
|
|
58
|
-
* <input @keydown.capture="log()">
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
|
-
* ### Key Modifiers
|
|
62
|
-
* ```html
|
|
63
|
-
* <input @keydown.enter="submit()">
|
|
64
|
-
* <input @keyup.escape="cancel()">
|
|
65
|
-
* <input @keydown.space="toggle()">
|
|
66
|
-
* <input @keydown.ctrl.s="save()">
|
|
67
|
-
* <input @keydown.meta.enter="send()">
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* ### Mouse Modifiers
|
|
71
|
-
* ```html
|
|
72
|
-
* <div @click.left="handleLeft()">
|
|
73
|
-
* <div @click.right="handleRight()">
|
|
74
|
-
* <div @click.middle="handleMiddle()">
|
|
75
|
-
* ```
|
|
76
|
-
*
|
|
77
|
-
* ### Special Variables
|
|
78
|
-
* ```html
|
|
79
|
-
* <input @input="value = $event.target.value">
|
|
80
|
-
* <button @click="handleClick($el)">Uses element</button>
|
|
81
|
-
* ```
|
|
82
|
-
*
|
|
83
|
-
* @module events
|
|
84
|
-
*/
|
|
85
62
|
// =============================================================================
|
|
86
63
|
// Types
|
|
87
64
|
// =============================================================================
|
package/dist/events.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { templateHasReactiveContext } from "./runtime-globals.js";
|
|
1
2
|
const DOM_EVENTS = new Set([
|
|
2
3
|
"click",
|
|
3
4
|
"dblclick",
|
|
@@ -185,8 +186,14 @@ function wrapHandler(handler, modifiers) {
|
|
|
185
186
|
wrapped = `__stx_throttle(() => { ${wrapped} }, ${modifiers.throttle})`;
|
|
186
187
|
return wrapped;
|
|
187
188
|
}
|
|
189
|
+
export function asInvocableStatement(handler) {
|
|
190
|
+
const name = handler.trim();
|
|
191
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name))
|
|
192
|
+
return handler;
|
|
193
|
+
return `if (typeof ${name} === 'function' && !${name}._isSignal) ${name}($event);`;
|
|
194
|
+
}
|
|
188
195
|
function generateEventListener(event) {
|
|
189
|
-
const { modifiers } = event, elementId = event.elementId.replace(/\\/g, "\\\\").replace(/'/g, "\\'"), checks = generateModifierChecks(event), handler = wrapHandler(event.handler, modifiers), options = [];
|
|
196
|
+
const { modifiers } = event, elementId = event.elementId.replace(/\\/g, "\\\\").replace(/'/g, "\\'"), checks = generateModifierChecks(event), handler = wrapHandler(asInvocableStatement(event.handler), modifiers), options = [];
|
|
190
197
|
if (modifiers.once)
|
|
191
198
|
options.push("once: true");
|
|
192
199
|
if (modifiers.capture)
|
|
@@ -254,8 +261,8 @@ else {
|
|
|
254
261
|
}${optionsStr});
|
|
255
262
|
})();`;
|
|
256
263
|
}
|
|
257
|
-
function generateRuntimeScript(
|
|
258
|
-
if (
|
|
264
|
+
export function generateRuntimeScript(events) {
|
|
265
|
+
if (events.length === 0)
|
|
259
266
|
return "";
|
|
260
267
|
return `
|
|
261
268
|
<script data-stx-events>
|
|
@@ -284,9 +291,16 @@ function generateRuntimeScript(elements) {
|
|
|
284
291
|
};
|
|
285
292
|
}
|
|
286
293
|
|
|
287
|
-
// Non-reactive fallback runner. The handler is embedded HTML-entity-encoded
|
|
288
|
-
// the surrounding
|
|
289
|
-
// decode it to JS only in memory here, then run it
|
|
294
|
+
// Non-reactive fallback runner. The handler is embedded HTML-entity-encoded
|
|
295
|
+
// so the surrounding script element can never contain raw markup, including
|
|
296
|
+
// its own closing tag; we decode it to JS only in memory here, then run it
|
|
297
|
+
// with $event/$el in scope.
|
|
298
|
+
//
|
|
299
|
+
// This comment must not spell that closing tag out. It used to, as an
|
|
300
|
+
// example, and a browser ends the script element at the FIRST one it sees \u2014
|
|
301
|
+
// so everything below this line, __stx_runHandler included, was emitted as
|
|
302
|
+
// page text and no listener was ever bound. The comment about escaping was
|
|
303
|
+
// the thing that needed escaping.
|
|
290
304
|
function __stx_runHandler(h, $event, $el) {
|
|
291
305
|
var code = h
|
|
292
306
|
.replace(/</g, '<')
|
|
@@ -305,7 +319,7 @@ function generateRuntimeScript(elements) {
|
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
// Event listeners
|
|
308
|
-
${
|
|
322
|
+
${events.map(generateEventListener).join(`
|
|
309
323
|
`)}
|
|
310
324
|
})();
|
|
311
325
|
</script>`;
|
|
@@ -338,7 +352,7 @@ export function extractAndProcessEvents(template) {
|
|
|
338
352
|
return { template: output, bindings: allBindings };
|
|
339
353
|
}
|
|
340
354
|
export function processEventDirectives(template, context, _filePath) {
|
|
341
|
-
if (
|
|
355
|
+
if (templateHasReactiveContext(template))
|
|
342
356
|
return template;
|
|
343
357
|
if (context.__stx_sfc_mode) {
|
|
344
358
|
if (/x-data\s*=\s*["']/.test(template))
|
|
@@ -369,7 +383,7 @@ export function processEventDirectives(template, context, _filePath) {
|
|
|
369
383
|
}
|
|
370
384
|
output = output.slice(0, element.startIndex) + newTag + output.slice(element.endIndex);
|
|
371
385
|
}
|
|
372
|
-
const script = generateRuntimeScript(elements), bodyIdx = output.lastIndexOf("</body>");
|
|
386
|
+
const script = generateRuntimeScript(elements.flatMap((el) => el.events)), bodyIdx = output.lastIndexOf("</body>");
|
|
373
387
|
if (bodyIdx !== -1)
|
|
374
388
|
output = output.slice(0, bodyIdx) + script + `
|
|
375
389
|
` + output.slice(bodyIdx);
|
package/dist/expressions.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare function interpolateScriptsInTemplate(template: string, context:
|
|
|
80
80
|
/**
|
|
81
81
|
* Process template expressions including variables, filters, and operations
|
|
82
82
|
*/
|
|
83
|
-
export declare function processExpressions(template: string, context: Record<string, any>, filePath: string
|
|
83
|
+
export declare function processExpressions(template: string, context: Record<string, any>, filePath: string): string;
|
|
84
84
|
/**
|
|
85
85
|
* Apply filters to a value
|
|
86
86
|
*/
|
package/dist/expressions.js
CHANGED
|
@@ -318,7 +318,7 @@ export function interpolateScriptExpressions(scriptBody, context) {
|
|
|
318
318
|
const value = evaluateExpression(expr, context);
|
|
319
319
|
if (value === void 0)
|
|
320
320
|
return match;
|
|
321
|
-
return JSON.stringify(value);
|
|
321
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
322
322
|
} catch {
|
|
323
323
|
return match;
|
|
324
324
|
}
|
|
@@ -344,7 +344,7 @@ export function interpolateScriptsInTemplate(template, context, options = {}) {
|
|
|
344
344
|
});
|
|
345
345
|
return restoreStashedScripts(output, interpolatedScripts);
|
|
346
346
|
}
|
|
347
|
-
export function processExpressions(template, context, filePath
|
|
347
|
+
export function processExpressions(template, context, filePath) {
|
|
348
348
|
let output = template;
|
|
349
349
|
const styleMatcher = (html, i) => {
|
|
350
350
|
const end = matchStyleElement(html, i);
|
|
@@ -359,7 +359,7 @@ export function processExpressions(template, context, filePath, options) {
|
|
|
359
359
|
output = maskInto(output, styleMatcher, styleBlocks, (n) => `<!--__STX_STYLE_${n}__-->`);
|
|
360
360
|
const scriptBlocks = [];
|
|
361
361
|
output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
|
|
362
|
-
const hasSignals =
|
|
362
|
+
const hasSignals = context?.__stx_signals_gate === !0 || usesSignalsInScript(template), clientSignalNames = new Set(Array.isArray(context?.__stx_client_signal_names) ? context.__stx_client_signal_names : []), preservedClientExpressions = new Set(Array.isArray(context?.__stx_preserved_client_expressions) ? context.__stx_preserved_client_expressions : []);
|
|
363
363
|
output = output.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (match, expr, offset) => {
|
|
364
364
|
try {
|
|
365
365
|
const value = evaluateExpression(expr, context);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Clear the cache. Dev HMR calls this when composables change on disk. */
|
|
2
|
+
export declare function clearFrameworkComposableCache(): void;
|
|
3
|
+
/**
|
|
4
|
+
* Choose which files in the composables directory carry runtime code.
|
|
5
|
+
*
|
|
6
|
+
* This ran against a `*.ts` glob, which is only ever right when stx runs from
|
|
7
|
+
* `src/`. A released package runs from `dist/`, where the directory holds 37
|
|
8
|
+
* `.js` files and 37 `.d.ts` files and not one plain `.ts` — so every file the
|
|
9
|
+
* bundler opened was type-only, nothing was ever emitted, and every requested
|
|
10
|
+
* name was reported as unbundlable. The one configuration where the old glob
|
|
11
|
+
* worked is the one the repo's own tests run in, which is why it never showed
|
|
12
|
+
* up here (stacksjs/stx#1832).
|
|
13
|
+
*
|
|
14
|
+
* Two compounding details, both fixed by going through this function:
|
|
15
|
+
*
|
|
16
|
+
* - `.d.ts` can never contribute runtime code, so it is excluded outright
|
|
17
|
+
* rather than left to be caught downstream.
|
|
18
|
+
* - The barrel exclusion compared `endsWith('index.ts')`, and `'index.d.ts'`
|
|
19
|
+
* does not end with that. The one file the filter existed to skip was the
|
|
20
|
+
* only one it let through — and being a barrel of `export … from …`, it was
|
|
21
|
+
* also the one shape the export-stripping below cannot parse.
|
|
22
|
+
*
|
|
23
|
+
* Exported so the selection can be tested against a dist-shaped listing without
|
|
24
|
+
* a built package.
|
|
25
|
+
*/
|
|
26
|
+
export declare function selectComposableFiles(files: string[]): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Which framework composables does this source actually call?
|
|
29
|
+
*
|
|
30
|
+
* Restricted to the server-only set, so a page calling `useLocalStorage` (which
|
|
31
|
+
* the runtime provides) never drags in a module copy.
|
|
32
|
+
*/
|
|
33
|
+
export declare function referencedFrameworkComposables(source: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Build a script exposing `names` on `window.stx` and as bare globals.
|
|
36
|
+
*
|
|
37
|
+
* Returns null when nothing is needed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getFrameworkComposableScript(source: string, composablesDir?: string): Promise<string | null>;
|
|
40
|
+
/** @internal test-only — declare names the runtime provides. */
|
|
41
|
+
export declare function __setRuntimeOwnedForTest(names: string[]): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getPublicEnvDefine } from "./public-env.js";
|
|
3
|
+
import { SERVER_ONLY_COMPOSABLES } from "./unresolved-identifiers.js";
|
|
4
|
+
const _cache = new Map, RUNTIME_OWNED = new Set, SERVER_ONLY = new Set(SERVER_ONLY_COMPOSABLES);
|
|
5
|
+
export function clearFrameworkComposableCache() {
|
|
6
|
+
_cache.clear();
|
|
7
|
+
}
|
|
8
|
+
export function selectComposableFiles(files) {
|
|
9
|
+
const byBase = new Map;
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const base = path.basename(file);
|
|
12
|
+
if (base.endsWith(".d.ts"))
|
|
13
|
+
continue;
|
|
14
|
+
const stem = base.replace(/\.[jt]s$/, "");
|
|
15
|
+
if (stem === "index")
|
|
16
|
+
continue;
|
|
17
|
+
if (stem === base)
|
|
18
|
+
continue;
|
|
19
|
+
const existing = byBase.get(stem);
|
|
20
|
+
if (existing && !existing.endsWith(".js"))
|
|
21
|
+
continue;
|
|
22
|
+
byBase.set(stem, file);
|
|
23
|
+
}
|
|
24
|
+
return [...byBase.values()].sort();
|
|
25
|
+
}
|
|
26
|
+
function declaresLocally(source, name) {
|
|
27
|
+
const escaped = name.replace(/[$()*+.?[\\\]^{|}-]/g, "\\$&");
|
|
28
|
+
return new RegExp(`(?:^|[^\\w$.])(?:const|let|var|function|class)\\s+${escaped}\\b`).test(source);
|
|
29
|
+
}
|
|
30
|
+
export function referencedFrameworkComposables(source) {
|
|
31
|
+
if (!source)
|
|
32
|
+
return [];
|
|
33
|
+
const authored = source.replace(/<script\b[^>]*\bdata-stx-runtime\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-framework-composables\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-stores\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\bdata-stx-composables\b[^>]*>[\s\S]*?<\/script>/gi, ""), found = [];
|
|
34
|
+
for (const name of SERVER_ONLY) {
|
|
35
|
+
if (RUNTIME_OWNED.has(name))
|
|
36
|
+
continue;
|
|
37
|
+
const escaped = name.replace(/[$()*+.?[\\\]^{|}-]/g, "\\$&");
|
|
38
|
+
if (new RegExp(`(^|[^\\w$.])${escaped}\\s*\\(`).test(authored))
|
|
39
|
+
found.push(name);
|
|
40
|
+
}
|
|
41
|
+
return found.sort();
|
|
42
|
+
}
|
|
43
|
+
export async function getFrameworkComposableScript(source, composablesDir) {
|
|
44
|
+
const names = referencedFrameworkComposables(source);
|
|
45
|
+
if (names.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
const dir = composablesDir ?? path.join(import.meta.dir, "composables"), key = `${dir}\x00${names.join(",")}`, cached = _cache.get(key);
|
|
48
|
+
if (cached !== void 0)
|
|
49
|
+
return cached || null;
|
|
50
|
+
const transpiler = new Bun.Transpiler({ loader: "ts", target: "browser", define: getPublicEnvDefine() }), wanted = new Set(names), chunks = [], emitted = new Set;
|
|
51
|
+
let files = [];
|
|
52
|
+
try {
|
|
53
|
+
const glob = new Bun.Glob("*.{ts,js}");
|
|
54
|
+
for await (const file of glob.scan({ cwd: dir, absolute: !0 }))
|
|
55
|
+
files.push(file);
|
|
56
|
+
} catch {
|
|
57
|
+
_cache.set(key, "");
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
files = selectComposableFiles(files);
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
let code;
|
|
63
|
+
try {
|
|
64
|
+
code = await Bun.file(file).text();
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
let exports = [];
|
|
69
|
+
try {
|
|
70
|
+
exports = transpiler.scan(code).exports.filter((n) => n !== "default");
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!exports.some((n) => wanted.has(n)))
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
code = code.replace(/^import\s+.*from\s+['"][^'"]+['"]\s*;?\s*$/gm, "");
|
|
78
|
+
code = code.replace(/^import\s+type\s+.*$/gm, "");
|
|
79
|
+
code = code.replace(/^export\s+\*\s+(?:as\s+\w+\s+)?from\s*['"][^'"]+['"]\s*;?\s*$/gm, "");
|
|
80
|
+
code = code.replace(/^export\s+(?:type\s+)?\{[\s\S]*?\}\s*(?:from\s*['"][^'"]+['"]\s*)?;?\s*$/gm, "");
|
|
81
|
+
code = code.replace(/^export\s+default\s+/gm, "");
|
|
82
|
+
code = code.replace(/^export\s+(?=(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface|enum)\b)/gm, "");
|
|
83
|
+
chunks.push(`// ${path.basename(file).replace(/\.[jt]s$/, "")}
|
|
84
|
+
${transpiler.transformSync(code).trim()}`);
|
|
85
|
+
for (const name of exports)
|
|
86
|
+
emitted.add(name);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn(`[stx] framework composable ${path.basename(file)} could not be bundled:`, error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const missing = names.filter((n) => !emitted.has(n) && !declaresLocally(source, n));
|
|
92
|
+
if (missing.length > 0)
|
|
93
|
+
console.warn(`[stx] could not bundle ${missing.join(", ")} from @stacksjs/stx/composables \u2014 ` + "calls to them will throw ReferenceError in the browser. Import them explicitly so the bundler inlines them, or use a runtime equivalent from window.stx.");
|
|
94
|
+
if (chunks.length === 0) {
|
|
95
|
+
_cache.set(key, "");
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const assignments = [...emitted].map((name) => ` try { if (!(${JSON.stringify(name)} in __s)) { __s[${JSON.stringify(name)}] = ${name}; if (!(${JSON.stringify(name)} in window)) window[${JSON.stringify(name)}] = ${name}; } } catch (e) {}`).join(`
|
|
99
|
+
`), script = `<script data-stx-framework-composables>
|
|
100
|
+
;(function(){
|
|
101
|
+
var __s = window.stx = window.stx || {};
|
|
102
|
+
${chunks.join(`
|
|
103
|
+
|
|
104
|
+
`)}
|
|
105
|
+
${assignments}
|
|
106
|
+
})();
|
|
107
|
+
</script>`;
|
|
108
|
+
_cache.set(key, script);
|
|
109
|
+
return script;
|
|
110
|
+
}
|
|
111
|
+
export function __setRuntimeOwnedForTest(names) {
|
|
112
|
+
RUNTIME_OWNED.clear();
|
|
113
|
+
for (const n of names)
|
|
114
|
+
RUNTIME_OWNED.add(n);
|
|
115
|
+
}
|
package/dist/head.js
CHANGED
|
@@ -194,7 +194,11 @@ export function applyHead(config = currentHead) {
|
|
|
194
194
|
}
|
|
195
195
|
if (config.htmlAttrs)
|
|
196
196
|
for (const [key, value] of Object.entries(config.htmlAttrs))
|
|
197
|
-
|
|
197
|
+
if (key === "class")
|
|
198
|
+
for (const cls of String(value).split(/\s+/).filter(Boolean))
|
|
199
|
+
document.documentElement.classList.add(cls);
|
|
200
|
+
else
|
|
201
|
+
document.documentElement.setAttribute(key, value);
|
|
198
202
|
if (config.bodyAttrs)
|
|
199
203
|
for (const [key, value] of Object.entries(config.bodyAttrs))
|
|
200
204
|
document.body.setAttribute(key, value);
|
package/dist/hot-reload.js
CHANGED
|
@@ -198,8 +198,14 @@ catch (e) {
|
|
|
198
198
|
console.log('[stx] Reloading...', msg.path || '');
|
|
199
199
|
// Use SPA router for progressive update instead of full page reload.
|
|
200
200
|
// Falls back to hard reload only when the router isn't available.
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// Call the ROUTER directly, not window.navigate: the runtime helper
|
|
202
|
+
// takes (url, options) and silently dropped the third argument, so
|
|
203
|
+
// force never arrived and the same-URL early return made this a
|
|
204
|
+
// no-op on every page that had the router (#1807). pushState=false
|
|
205
|
+
// keeps the address bar untouched (this is a reload, not a
|
|
206
|
+
// navigation) and force=true bypasses the early return.
|
|
207
|
+
if (window.stxRouter && typeof window.stxRouter.navigate === 'function') {
|
|
208
|
+
window.stxRouter.navigate(window.location.pathname + window.location.search, false, true);
|
|
203
209
|
} else {
|
|
204
210
|
window.location.reload();
|
|
205
211
|
}
|
package/dist/includes.js
CHANGED
|
@@ -560,6 +560,7 @@ export async function processIncludes(template, context, filePath, options, depe
|
|
|
560
560
|
workingContent = workingContent.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
|
|
561
561
|
workingContent = workingContent.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
562
562
|
const includeContext = { ...context };
|
|
563
|
+
includeContext.__stx_signals_gate = usesSignalsInScript(partialContent);
|
|
563
564
|
for (const [key, value] of Object.entries(localVars))
|
|
564
565
|
includeContext[key] = value;
|
|
565
566
|
const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi, scriptMatches = [...partialContent.matchAll(scriptRegex)];
|
|
@@ -597,7 +598,7 @@ export async function processIncludes(template, context, filePath, options, depe
|
|
|
597
598
|
if (isSignalScript && !isServerScript) {
|
|
598
599
|
signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
|
|
599
600
|
const resolvedContent = transformStoreImports(scriptContent), transformedScript = transformSignalScript(resolvedContent, signalScopeId);
|
|
600
|
-
preservedScript += `${vendorStyleTags}<script data-stx-scoped>${transformedScript}</script>
|
|
601
|
+
preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always">${transformedScript}</script>
|
|
601
602
|
`;
|
|
602
603
|
continue;
|
|
603
604
|
}
|
|
@@ -613,7 +614,7 @@ export async function processIncludes(template, context, filePath, options, depe
|
|
|
613
614
|
const extraAttrs = scriptAttrs.replace(/\bclient\b/i, "").trim();
|
|
614
615
|
signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
|
|
615
616
|
const resolvedContent = transformStoreImports(scriptContent), tail = buildScopeRegistrationTail(resolvedContent, signalScopeId), wrapped = wrapClientScript(resolvedContent, tail);
|
|
616
|
-
preservedScript += `${vendorStyleTags}<script data-stx-scoped${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
|
|
617
|
+
preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always"${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
|
|
617
618
|
`;
|
|
618
619
|
}
|
|
619
620
|
}
|
|
@@ -630,8 +631,7 @@ export async function processIncludes(template, context, filePath, options, depe
|
|
|
630
631
|
const { processLoops } = await import("./loops.js");
|
|
631
632
|
let processedContent = processLoops(workingContent, includeContext, includeFilePath);
|
|
632
633
|
processedContent = processConditionals(processedContent, includeContext, includeFilePath);
|
|
633
|
-
|
|
634
|
-
processedContent = processExpressions(processedContent, includeContext, includeFilePath, { forceSignals: partialHasSignals });
|
|
634
|
+
processedContent = processExpressions(processedContent, includeContext, includeFilePath);
|
|
635
635
|
if (preservedStyle)
|
|
636
636
|
processedContent += `
|
|
637
637
|
` + preservedStyle;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,22 @@ export { type HydrationOptions } from './hydration';
|
|
|
28
28
|
// shapes with different field sets; those are kept private to their modules
|
|
29
29
|
// to avoid silently shadowing the user-facing one. Surfaced by stacksjs/stx#1707.
|
|
30
30
|
export { type CookieOptions } from './composables/use-cookie';
|
|
31
|
+
// Storage composables were reachable ONLY via the './composables' subpath, so
|
|
32
|
+
// `import { useLocalStorage } from '@stacksjs/stx'` failed with TS2614 while
|
|
33
|
+
// `state`/`derived` imported fine — and a `functions/` composable (which the
|
|
34
|
+
// docs recommend for anything with persistence) had to reach for the subpath,
|
|
35
|
+
// where the same identifier used to return a different shape. See #1797.
|
|
36
|
+
export {
|
|
37
|
+
clearStorage,
|
|
38
|
+
getStorageKeys,
|
|
39
|
+
getStorageSize,
|
|
40
|
+
type StorageRef,
|
|
41
|
+
type StorageType,
|
|
42
|
+
useLocalStorage,
|
|
43
|
+
useSessionStorage,
|
|
44
|
+
useStorage,
|
|
45
|
+
type UseStorageOptions,
|
|
46
|
+
} from './composables/use-storage';
|
|
31
47
|
export { formatSize, getTotalSize } from './deploy/index';
|
|
32
48
|
export { type ImageRenderResult, type ImageVariant, type ProcessedImage, clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from './image-optimization/index';
|
|
33
49
|
export { type AnalysisResult } from './analyzer';
|
|
@@ -62,6 +78,7 @@ export * from './async-components';
|
|
|
62
78
|
export * from './auth';
|
|
63
79
|
export * from './build';
|
|
64
80
|
export * from './build-assets';
|
|
81
|
+
export * from './build-id';
|
|
65
82
|
export * from './build-mode-detector';
|
|
66
83
|
export * from './build-optimizer';
|
|
67
84
|
export * from './broadcasting';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
clearStorage,
|
|
3
|
+
getStorageKeys,
|
|
4
|
+
getStorageSize,
|
|
5
|
+
useLocalStorage,
|
|
6
|
+
useSessionStorage,
|
|
7
|
+
useStorage
|
|
8
|
+
} from "./composables/use-storage.js";
|
|
1
9
|
export { formatSize, getTotalSize } from "./deploy/index.js";
|
|
2
10
|
export { clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from "./image-optimization/index.js";
|
|
3
11
|
export { fileExists } from "./utils.js";
|
|
@@ -21,6 +29,7 @@ export * from "./async-components.js";
|
|
|
21
29
|
export * from "./auth.js";
|
|
22
30
|
export * from "./build.js";
|
|
23
31
|
export * from "./build-assets.js";
|
|
32
|
+
export * from "./build-id.js";
|
|
24
33
|
export * from "./build-mode-detector.js";
|
|
25
34
|
export * from "./build-optimizer.js";
|
|
26
35
|
export * from "./broadcasting.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `build` over `entrypoints`, falling back to one-at-a-time on failure.
|
|
3
|
+
*
|
|
4
|
+
* @param entrypoints absolute paths to build
|
|
5
|
+
* @param build performs a Bun.build over the given subset; should throw or
|
|
6
|
+
* reject on failure, which is what Bun.build does by default
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildIsolatingFailures(entrypoints: string[], build: (subset: string[]) => Promise<unknown>): Promise<IsolatedBuildResult>;
|
|
9
|
+
export declare interface IsolatedBuildResult {
|
|
10
|
+
succeeded: string[]
|
|
11
|
+
failed: Array<{ entrypoint: string, error: string }>
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { formatBuildFailure } from "./build-message.js";
|
|
2
|
+
export async function buildIsolatingFailures(entrypoints, build) {
|
|
3
|
+
if (entrypoints.length === 0)
|
|
4
|
+
return { succeeded: [], failed: [] };
|
|
5
|
+
try {
|
|
6
|
+
await build(entrypoints);
|
|
7
|
+
return { succeeded: [...entrypoints], failed: [] };
|
|
8
|
+
} catch {}
|
|
9
|
+
const succeeded = [], failed = [];
|
|
10
|
+
for (const entrypoint of entrypoints)
|
|
11
|
+
try {
|
|
12
|
+
await build([entrypoint]);
|
|
13
|
+
succeeded.push(entrypoint);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
failed.push({ entrypoint, error: formatBuildFailure(error, entrypoint) });
|
|
16
|
+
}
|
|
17
|
+
return { succeeded, failed };
|
|
18
|
+
}
|