@stacksjs/stx 0.2.153 → 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 +1 -27
- 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 +1481 -1231
- package/dist/client-script-bundler.js +2 -4
- package/dist/client-script.d.ts +29 -0
- package/dist/client-script.js +140 -95
- package/dist/color-mode-boot.d.ts +31 -0
- package/dist/color-mode-boot.js +41 -5
- package/dist/component-renderer.js +10 -21
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/use-clipboard.d.ts +1 -5
- package/dist/composables/use-clipboard.js +57 -46
- package/dist/composables/use-color-mode.js +13 -5
- package/dist/composables/use-router.d.ts +28 -11
- package/dist/composables/use-router.js +63 -9
- package/dist/config.js +22 -7
- package/dist/craft.js +1029 -789
- package/dist/dev-server/crosswind.js +8 -6
- package/dist/dev-server/serve-app.js +4 -2
- package/dist/dev-server/serve-markdown.js +4 -1
- package/dist/dev-server/serve-multi.js +32 -20
- package/dist/document-shell.d.ts +65 -0
- package/dist/document-shell.js +59 -12
- 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/hot-reload.js +8 -2
- package/dist/includes.js +2 -2
- 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 +40 -10
- 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 +966 -726
- 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 +258 -47
- 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 +986 -746
- 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 +111 -16
- 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 +111 -16
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/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)];
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
package/dist/plugin-system.d.ts
CHANGED
|
@@ -89,7 +89,7 @@ export declare interface StxPlugin {
|
|
|
89
89
|
registerFilters?: () => Record<string, (value: unknown, ...args: unknown[]) => unknown>
|
|
90
90
|
onRegister?: (options: StxOptions) => void | Promise<void>
|
|
91
91
|
onUnregister?: () => void | Promise<void>
|
|
92
|
-
components?: string
|
|
92
|
+
components?: string | string[]
|
|
93
93
|
functions?: string
|
|
94
94
|
stores?: string
|
|
95
95
|
pages?: string
|
package/dist/process.js
CHANGED
|
@@ -7,7 +7,7 @@ import { processTemplateBindings } from "./reactive-bindings.js";
|
|
|
7
7
|
import { injectAnalytics } from "./analytics.js";
|
|
8
8
|
import { injectHeatmap } from "./heatmap.js";
|
|
9
9
|
import { processAnimationDirectives } from "./animation.js";
|
|
10
|
-
import { processEventDirectives } from "./events.js";
|
|
10
|
+
import { generateRuntimeScript, processEventDirectives } from "./events.js";
|
|
11
11
|
import { extractBridgeData, processClientScript } from "./client-script.js";
|
|
12
12
|
import { processReactiveDirectives } from "./reactive.js";
|
|
13
13
|
import { processMarkdownFileDirectives } from "./assets.js";
|
|
@@ -36,9 +36,9 @@ import { processServerBindings } from "./server-bindings.js";
|
|
|
36
36
|
import { processVueTemplate } from "./vue-template.js";
|
|
37
37
|
import { processDynamicComponents } from "./dynamic-components.js";
|
|
38
38
|
import { dedupeScopedStyles, processScopedStyles } from "./style-scoping.js";
|
|
39
|
-
import { injectColorModeBootScript } from "./color-mode-boot.js";
|
|
39
|
+
import { injectColorModeBootScript, normalizeCriticalHeadOrder } from "./color-mode-boot.js";
|
|
40
40
|
import { injectBuildId } from "./build-id.js";
|
|
41
|
-
import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs } from "./document-shell.js";
|
|
41
|
+
import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs, startsDocument } from "./document-shell.js";
|
|
42
42
|
import { hasSignalsSyntax, convertSignalDirectivesToAttributes, convertSignalLoopsToAttributes, preEvalLiteralReactiveIfs, processSignals } from "./signal-processing.js";
|
|
43
43
|
import { processComponents } from "./component-renderer.js";
|
|
44
44
|
import { processInlineAssets } from "./inline-assets.js";
|
|
@@ -51,6 +51,7 @@ import { deriveLayoutGroup } from "stx-router/layout-metadata";
|
|
|
51
51
|
import { initializeComponentClientFactories, injectComponentClientFactories } from "./component-client-factories.js";
|
|
52
52
|
import { importOnce } from "./lazy-module.js";
|
|
53
53
|
export { injectRouterScript } from "./runtime-injection.js";
|
|
54
|
+
import { injectSignalsRuntime, outputNeedsSignalsRuntime, pageShipsSignalsRuntime } from "./runtime-injection.js";
|
|
54
55
|
export { processJsonDirective, processOnceDirective } from "./misc-directives.js";
|
|
55
56
|
export { validateClientScript } from "./script-validation.js";
|
|
56
57
|
const PARENT_PLACEHOLDER = "<!--PARENT_CONTENT_PLACEHOLDER-->";
|
|
@@ -246,6 +247,8 @@ function placeSignalsRuntimeBeforeScripts(html) {
|
|
|
246
247
|
export async function processDirectives(template, context, filePath, options, dependencies) {
|
|
247
248
|
if (options.ssr === !1 && !options.autoShell && options.buildMode === "spa")
|
|
248
249
|
return template;
|
|
250
|
+
if (context.__stx_signals_gate === void 0)
|
|
251
|
+
context.__stx_signals_gate = usesSignalsInScript(template);
|
|
249
252
|
const isTopLevel = !context.__stxProcessingDepth;
|
|
250
253
|
if (isTopLevel)
|
|
251
254
|
initializeComponentClientFactories(context);
|
|
@@ -269,7 +272,7 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
269
272
|
pageHeadTitle = rawTitleMatch[1].trim();
|
|
270
273
|
pageHeadRaw = pageHeadRaw.replace(rawTitleMatch[0], "").trim();
|
|
271
274
|
}
|
|
272
|
-
const sectionTitle = context.__sections?.title, pageTitle = runtimeHead.title || pageHeadTitle || sectionTitle, headConfig = {
|
|
275
|
+
const sectionTitle = context.__sections?.title, pageTitle = runtimeHead.title || pageHeadTitle || sectionTitle, titleIsHtml = !runtimeHead.title && (!!pageHeadTitle || !!sectionTitle), headConfig = {
|
|
273
276
|
...baseHeadConfig,
|
|
274
277
|
...pageTitle && { title: pageTitle },
|
|
275
278
|
meta: [...baseHeadConfig.meta || [], ...runtimeHead.meta || []],
|
|
@@ -281,6 +284,7 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
281
284
|
...runtimeHead.bodyAttrs && { bodyAttrs: { ...baseHeadConfig.bodyAttrs || {}, ...runtimeHead.bodyAttrs } }
|
|
282
285
|
}, alreadyShelled = hasDocumentShell(result);
|
|
283
286
|
result = ensureDocumentShell(result, headConfig, {
|
|
287
|
+
...titleIsHtml && { titleIsHtml: !0 },
|
|
284
288
|
...options.app?.colorMode && { colorMode: options.app.colorMode }
|
|
285
289
|
});
|
|
286
290
|
if (alreadyShelled) {
|
|
@@ -290,7 +294,7 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
290
294
|
const layoutComment = result.match(/<!-- stx-layout: ([^ ]+) -->/);
|
|
291
295
|
if (layoutComment && result.includes("<head")) {
|
|
292
296
|
const layoutName = layoutComment[1], layoutGroup = deriveLayoutGroup(layoutName);
|
|
293
|
-
result = result.replace(
|
|
297
|
+
result = result.replace(/<head\b[^>]*>/i, (match) => `${match}
|
|
294
298
|
<meta name="stx-layout" content="${escapeHtmlAttribute(layoutName)}">
|
|
295
299
|
<meta name="stx-layout-group" content="${escapeHtmlAttribute(layoutGroup)}">`);
|
|
296
300
|
}
|
|
@@ -319,6 +323,16 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
319
323
|
` + storeTag + result.slice(runtime.end);
|
|
320
324
|
}
|
|
321
325
|
} catch {}
|
|
326
|
+
if (isTopLevel)
|
|
327
|
+
try {
|
|
328
|
+
const { getFrameworkComposableScript } = await importOnce("stx/framework-composables", () => import("./framework-composables.js")), frameworkScript = await getFrameworkComposableScript(result);
|
|
329
|
+
if (frameworkScript) {
|
|
330
|
+
const anchor = findScriptBlockByAttribute(result, "data-stx-stores") || findScriptBlockByAttribute(result, "data-stx-runtime");
|
|
331
|
+
if (anchor)
|
|
332
|
+
result = result.slice(0, anchor.end) + `
|
|
333
|
+
` + frameworkScript + result.slice(anchor.end);
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
322
336
|
if (isTopLevel)
|
|
323
337
|
try {
|
|
324
338
|
const { getComposableScript } = await importOnce("stx/composable-loader", () => import("./composable-loader.js")), resolvedComposablesDir = options.composablesDir, composableCode = await getComposableScript(resolvedComposablesDir);
|
|
@@ -329,6 +343,8 @@ export async function processDirectives(template, context, filePath, options, de
|
|
|
329
343
|
` + composableTag + result.slice(anchor.end);
|
|
330
344
|
}
|
|
331
345
|
} catch {}
|
|
346
|
+
if (isTopLevel)
|
|
347
|
+
result = normalizeCriticalHeadOrder(result);
|
|
332
348
|
if (isTopLevel && context.__stx_inject_css !== !1)
|
|
333
349
|
result = await injectCrosswindCSS(result, void 0, options.buildMode === "serve");
|
|
334
350
|
if (isTopLevel)
|
|
@@ -384,8 +400,8 @@ async function processDirectivesInternal(template, context, filePath, options, d
|
|
|
384
400
|
output = output.replace(/@(?:layout|extends)\(\s*['"]([^'"]+)['"]\s*\)/g, "");
|
|
385
401
|
}
|
|
386
402
|
if (!layoutPath && !hasNoLayout && opts.defaultLayout) {
|
|
387
|
-
const
|
|
388
|
-
if (!
|
|
403
|
+
const isDocument = startsDocument(output), hasSections = /@section\s*\(/.test(output);
|
|
404
|
+
if (!isDocument || hasSections) {
|
|
389
405
|
let autoLayoutPath = "", searchDir = path.dirname(filePath);
|
|
390
406
|
const rootDir = opts.layoutsDir ? path.dirname(opts.layoutsDir) : process.cwd();
|
|
391
407
|
for (let i = 0;i < 20; i++) {
|
|
@@ -602,8 +618,8 @@ async function processOtherDirectives(template, context, filePath, options, depe
|
|
|
602
618
|
const { processStreamDirectives } = await importOnce("stx/streaming", () => import("./streaming.js"));
|
|
603
619
|
output = processStreamDirectives(output, context);
|
|
604
620
|
output = processLoops(output, context, filePath, opts);
|
|
605
|
-
output = await processComponents(output, context, filePath, opts, dependencies);
|
|
606
621
|
output = await processDynamicComponents(output, context, filePath, opts, dependencies);
|
|
622
|
+
output = await processComponents(output, context, filePath, opts, dependencies);
|
|
607
623
|
output = processAnimationDirectives(output, context, filePath, opts);
|
|
608
624
|
const { processDeferDirectives } = await importOnce("stx/defer", () => import("./defer.js"));
|
|
609
625
|
output = processDeferDirectives(output, context, filePath);
|
|
@@ -739,7 +755,7 @@ ${snippet}
|
|
|
739
755
|
}
|
|
740
756
|
const hasClientScripts = /<script\b(?![^>]*\bserver\b)[^>]*>[\s\S]*?<\/script>/i.test(output), hasStxUsage = /\bSTX\.\w+/.test(output), alreadyHasRuntime = /window\.STX\s*=/.test(output);
|
|
741
757
|
if (hasClientScripts && hasStxUsage && !alreadyHasRuntime) {
|
|
742
|
-
const runtimeScript = `<script data-stx-scoped>
|
|
758
|
+
const runtimeScript = `<script data-stx-scoped data-stx-run="once">
|
|
743
759
|
${generateLifecycleRuntime()}
|
|
744
760
|
</script>`;
|
|
745
761
|
if (output.includes("</head>")) {
|
|
@@ -789,12 +805,26 @@ ${runtimeScript}`);
|
|
|
789
805
|
for (const { match, attrs, content } of clientScriptMatches) {
|
|
790
806
|
clientScriptsTransformed = !0;
|
|
791
807
|
let processedContent = content;
|
|
792
|
-
validateClientScript(content, filePath, options.strict);
|
|
808
|
+
validateClientScript(content, filePath, options.strict, context.__originalFilePath);
|
|
793
809
|
const scriptResult = await processClientScript(content, { eventBindings, attrs, hasColonDirectives, templateContent: template, filePath, projectRoot: process.cwd(), production: options.buildMode === "compile", serverData });
|
|
794
810
|
output = output.replace(match, () => scriptResult);
|
|
795
811
|
}
|
|
796
812
|
if (clientScriptsTransformed)
|
|
797
813
|
context.__stx_event_bindings = [];
|
|
814
|
+
if (!clientScriptsTransformed && context.__stxProcessingDepth === 1) {
|
|
815
|
+
const orphaned = context.__stx_event_bindings || [];
|
|
816
|
+
if (orphaned.length > 0) {
|
|
817
|
+
const script = generateRuntimeScript(orphaned);
|
|
818
|
+
if (script) {
|
|
819
|
+
const bodyIdx = output.lastIndexOf("</body>");
|
|
820
|
+
output = bodyIdx === -1 ? output + script : output.slice(0, bodyIdx) + script + `
|
|
821
|
+
` + output.slice(bodyIdx);
|
|
822
|
+
}
|
|
823
|
+
context.__stx_event_bindings = [];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (!pageShipsSignalsRuntime(output) && outputNeedsSignalsRuntime(output))
|
|
827
|
+
output = await injectSignalsRuntime(output, opts);
|
|
798
828
|
if (opts.csp?.enabled && opts.csp.useNonce && typeof context.cspNonce === "string")
|
|
799
829
|
output = addNonceToInlineContent(output, context.cspNonce);
|
|
800
830
|
return output;
|
|
@@ -4,6 +4,7 @@ import { serve } from "bun";
|
|
|
4
4
|
import { loadManifest } from "./manifest.js";
|
|
5
5
|
import { hydrateTemplateStream, hydrateFragment } from "./template-hydrator.js";
|
|
6
6
|
import { extractLayoutMetadata } from "./app-shell.js";
|
|
7
|
+
import { pageShipsSignalsRuntime } from "./runtime-injection.js";
|
|
7
8
|
import { patternToRegex } from "stx-router";
|
|
8
9
|
const MIME_TYPES = {
|
|
9
10
|
".js": "application/javascript",
|
|
@@ -41,12 +42,13 @@ export async function startProductionServer(options = {}) {
|
|
|
41
42
|
process.exit(1);
|
|
42
43
|
}
|
|
43
44
|
console.log(`[stx] Production server loading ${manifest.routes.length} routes...`);
|
|
44
|
-
const compiledTemplates = new Map, fragmentCache = new Map, layoutMetadataCache = new Map;
|
|
45
|
+
const compiledTemplates = new Map, fragmentCache = new Map, layoutMetadataCache = new Map, runtimeCache = new Map;
|
|
45
46
|
for (const route of manifest.routes)
|
|
46
47
|
try {
|
|
47
48
|
const compiledPath = path.join(outputDir, route.compiledPath), compiled = JSON.parse(await Bun.file(compiledPath).text());
|
|
48
49
|
compiledTemplates.set(route.pattern, compiled);
|
|
49
50
|
layoutMetadataCache.set(route.pattern, extractLayoutMetadata(compiled.html));
|
|
51
|
+
runtimeCache.set(route.pattern, pageShipsSignalsRuntime(compiled.html));
|
|
50
52
|
const fragmentPath = path.join(outputDir, route.fragmentPath);
|
|
51
53
|
if (await Bun.file(fragmentPath).exists())
|
|
52
54
|
fragmentCache.set(route.pattern, await Bun.file(fragmentPath).text());
|
|
@@ -129,6 +131,7 @@ export async function startProductionServer(options = {}) {
|
|
|
129
131
|
headers: {
|
|
130
132
|
"Content-Type": "text/html",
|
|
131
133
|
"X-STX-Fragment": "true",
|
|
134
|
+
...runtimeCache.has(matchedRoute.pattern) ? { "X-STX-Runtime": runtimeCache.get(matchedRoute.pattern) ? "true" : "false" } : {},
|
|
132
135
|
...layoutMetadata ? {
|
|
133
136
|
"X-STX-Layout": layoutMetadata.layout,
|
|
134
137
|
"X-STX-Layout-Group": layoutMetadata.group
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Is this value the bare-boolean-attribute marker? */
|
|
2
|
+
export declare function isBooleanAttributeSentinel(value: unknown): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Last-resort guard for anything about to be written into a response.
|
|
5
|
+
*
|
|
6
|
+
* Nothing should reach output carrying a NUL, but the cost of one escaping is
|
|
7
|
+
* an invalid document and a silent grep, which is far worse than the cost of
|
|
8
|
+
* this check.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripControlChars(value: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Sentinel values used while resolving component props.
|
|
13
|
+
*
|
|
14
|
+
* A leaf module on purpose: `component-renderer.ts` imports `./builtins`, so a
|
|
15
|
+
* builtin importing the renderer back would be a cycle. Both sides import this
|
|
16
|
+
* instead.
|
|
17
|
+
*
|
|
18
|
+
* @module prop-sentinels
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Marks a prop that was written as a bare HTML boolean attribute — `<X open>`
|
|
22
|
+
* rather than `<X open="true">`.
|
|
23
|
+
*
|
|
24
|
+
* It carries a leading NUL precisely so it cannot collide with an author-written
|
|
25
|
+
* value, which also means it must never survive into a response: a NUL byte
|
|
26
|
+
* makes the document invalid UTF-8, and tools treat the file as binary. `grep`
|
|
27
|
+
* in particular goes SILENT on it — not zero matches, no output at all — so a
|
|
28
|
+
* verification step that greps rendered HTML returns an empty string and reads
|
|
29
|
+
* as a pass (stacksjs/stx#1816).
|
|
30
|
+
*/
|
|
31
|
+
export declare const BOOLEAN_ATTRIBUTE_SENTINEL: '\0stx-boolean-attribute';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const BOOLEAN_ATTRIBUTE_SENTINEL = "\x00stx-boolean-attribute";
|
|
2
|
+
export function isBooleanAttributeSentinel(value) {
|
|
3
|
+
return value === BOOLEAN_ATTRIBUTE_SENTINEL;
|
|
4
|
+
}
|
|
5
|
+
export function stripControlChars(value) {
|
|
6
|
+
return value.indexOf("\x00") === -1 ? value : value.replace(/\0/g, "");
|
|
7
|
+
}
|