@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
|
@@ -329,21 +329,23 @@ export async function generateCrosswindCSS(htmlContent, appDir) {
|
|
|
329
329
|
const safelist = [
|
|
330
330
|
...baseConfig.safelist || [],
|
|
331
331
|
...userConfig.safelist || []
|
|
332
|
-
],
|
|
332
|
+
], preflights = [
|
|
333
|
+
...baseConfig.preflights || [],
|
|
334
|
+
...userConfig.preflights || []
|
|
335
|
+
], includePreflight = userConfig.preflight !== !1, minify = userConfig.minify === !0, crosswindConfig = {
|
|
333
336
|
...baseConfig,
|
|
334
337
|
...userConfig,
|
|
335
338
|
content: [],
|
|
336
339
|
output: "",
|
|
337
|
-
preflight: !0,
|
|
338
|
-
minify: !1,
|
|
339
340
|
theme,
|
|
340
|
-
safelist
|
|
341
|
+
safelist,
|
|
342
|
+
...preflights.length > 0 && { preflights }
|
|
341
343
|
}, generator = new hw.CSSGenerator(crosswindConfig);
|
|
342
344
|
for (const cls of safelist)
|
|
343
345
|
generator.generate(cls);
|
|
344
346
|
for (const className of classes)
|
|
345
347
|
generator.generate(className);
|
|
346
|
-
let css = generator.toCSS(
|
|
348
|
+
let css = generator.toCSS(includePreflight, minify);
|
|
347
349
|
const shortcuts = crosswindConfig.shortcuts || userConfig.shortcuts || {};
|
|
348
350
|
for (const [name, classStr] of Object.entries(shortcuts)) {
|
|
349
351
|
if (!classes.has(name) && !safelist.includes(name))
|
|
@@ -352,7 +354,7 @@ export async function generateCrosswindCSS(htmlContent, appDir) {
|
|
|
352
354
|
for (const p of parts)
|
|
353
355
|
generator.generate(p);
|
|
354
356
|
}
|
|
355
|
-
css = generator.toCSS(
|
|
357
|
+
css = generator.toCSS(includePreflight, minify);
|
|
356
358
|
const cssLines = css.split(`
|
|
357
359
|
`);
|
|
358
360
|
for (const [name, classStr] of Object.entries(shortcuts)) {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
stopHmrServer
|
|
12
12
|
} from "../hot-reload.js";
|
|
13
13
|
import { partialsCache } from "../includes.js";
|
|
14
|
+
import { pageShipsSignalsRuntime } from "../runtime-injection.js";
|
|
14
15
|
import { BUILD_ID_HEADER, getBuildId } from "../build-id.js";
|
|
15
16
|
import { stateDir } from "../state-dir.js";
|
|
16
17
|
import { plugin as stxPlugin } from "../plugin.js";
|
|
@@ -257,7 +258,7 @@ export async function serveApp(appDir = ".", options = {}) {
|
|
|
257
258
|
} else
|
|
258
259
|
output = await processDirectives(templateContent, context, route.filePath, merged, dependencies);
|
|
259
260
|
if (!shell)
|
|
260
|
-
output = await injectRouterScript(output);
|
|
261
|
+
output = await injectRouterScript(output, { router: merged.router });
|
|
261
262
|
{
|
|
262
263
|
const { interpolateScriptExpressions } = await import("../expressions.js");
|
|
263
264
|
output = output.replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/g, (full, attrs, body) => {
|
|
@@ -436,7 +437,7 @@ export async function serveApp(appDir = ".", options = {}) {
|
|
|
436
437
|
}
|
|
437
438
|
let content = builtPage.content;
|
|
438
439
|
if (shell && isSpaNavigation(request)) {
|
|
439
|
-
const layoutMetadata = extractLayoutMetadata(content), titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i), pageTitle = titleMatch ? titleMatch[1].trim() : "";
|
|
440
|
+
const layoutMetadata = extractLayoutMetadata(content), shipsRuntime = pageShipsSignalsRuntime(content), titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i), pageTitle = titleMatch ? titleMatch[1].trim() : "";
|
|
440
441
|
if (Object.keys(routeMatch.params).length > 0)
|
|
441
442
|
content = injectRouteParams(content, routeMatch.params);
|
|
442
443
|
content = await injectCrosswindCSS(content, absoluteAppDir);
|
|
@@ -449,6 +450,7 @@ export async function serveApp(appDir = ".", options = {}) {
|
|
|
449
450
|
"X-STX-Fragment": "true",
|
|
450
451
|
"X-STX-Layout": layoutMetadata.layout,
|
|
451
452
|
"X-STX-Layout-Group": layoutMetadata.group,
|
|
453
|
+
"X-STX-Runtime": shipsRuntime ? "true" : "false",
|
|
452
454
|
...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
|
|
453
455
|
[BUILD_ID_HEADER]: getBuildId(),
|
|
454
456
|
"Cache-Control": "no-store, no-cache, must-revalidate"
|
|
@@ -44,7 +44,7 @@ export async function serveMarkdownFile(filePath, options = {}) {
|
|
|
44
44
|
<head>
|
|
45
45
|
<meta charset="UTF-8">
|
|
46
46
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
47
|
-
<title>${data.title || path.basename(absolutePath)}</title>
|
|
47
|
+
<title>${escapeTitle(data.title || path.basename(absolutePath))}</title>
|
|
48
48
|
<!-- Syntax highlighting styles -->
|
|
49
49
|
<style id="syntax-theme">
|
|
50
50
|
:root {
|
|
@@ -279,3 +279,6 @@ Press ${colors.cyan}h${colors.reset} + ${colors.cyan}Enter${colors.reset} to sho
|
|
|
279
279
|
}
|
|
280
280
|
return !0;
|
|
281
281
|
}
|
|
282
|
+
function escapeTitle(value) {
|
|
283
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
284
|
+
}
|
|
@@ -8,6 +8,7 @@ import { partialsCache } from "../includes.js";
|
|
|
8
8
|
import { stateDir } from "../state-dir.js";
|
|
9
9
|
import { plugin as stxPlugin } from "../plugin.js";
|
|
10
10
|
import { clearComponentCache } from "../utils.js";
|
|
11
|
+
import { formatBuildFailure } from "../build-message.js";
|
|
11
12
|
import { bracketPathToRegex } from "stx-router";
|
|
12
13
|
import {
|
|
13
14
|
colors,
|
|
@@ -73,7 +74,7 @@ export async function serveMultipleStxFiles(filePaths, options = {}) {
|
|
|
73
74
|
<head>
|
|
74
75
|
<meta charset="UTF-8">
|
|
75
76
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
76
|
-
<title>${data.title || path.basename(absolutePath)}</title>
|
|
77
|
+
<title>${escapeTitle(data.title || path.basename(absolutePath))}</title>
|
|
77
78
|
<!-- Syntax highlighting styles -->
|
|
78
79
|
<style id="syntax-theme">
|
|
79
80
|
:root {
|
|
@@ -253,25 +254,33 @@ export async function serveMultipleStxFiles(filePaths, options = {}) {
|
|
|
253
254
|
};
|
|
254
255
|
});
|
|
255
256
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
257
|
+
};
|
|
258
|
+
let htmlContent;
|
|
259
|
+
try {
|
|
260
|
+
const result = await Bun.build({
|
|
261
|
+
entrypoints: [absolutePath],
|
|
262
|
+
outdir: outputDir,
|
|
263
|
+
plugins: [publicAssetsPlugin, stxPlugin],
|
|
264
|
+
define: {
|
|
265
|
+
"process.env.NODE_ENV": '"development"'
|
|
266
|
+
},
|
|
267
|
+
...options.stxOptions
|
|
268
|
+
});
|
|
269
|
+
if (!result.success) {
|
|
270
|
+
console.error(`${colors.red}Build failed for ${colors.bright}${filePath}${colors.reset}:`, formatBuildFailure(result.logs, absolutePath));
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const htmlOutput = result.outputs.find((o) => o.path.endsWith(".html"));
|
|
274
|
+
if (!htmlOutput) {
|
|
275
|
+
console.error(`${colors.red}No HTML output found for ${colors.bright}${filePath}${colors.reset}`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
htmlContent = await Bun.file(htmlOutput.path).text();
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error(`${colors.red}Build failed for ${colors.bright}${filePath}${colors.reset}:
|
|
281
|
+
${formatBuildFailure(error, absolutePath)}`);
|
|
272
282
|
continue;
|
|
273
283
|
}
|
|
274
|
-
const htmlContent = await Bun.file(htmlOutput.path).text();
|
|
275
284
|
let routePath = `/${path.relative(commonDir, absolutePath).replace(/\.stx$/, "")}`;
|
|
276
285
|
if (routePath.endsWith("/index"))
|
|
277
286
|
routePath = routePath.slice(0, -6) || "/";
|
|
@@ -303,7 +312,7 @@ export async function serveMultipleStxFiles(filePaths, options = {}) {
|
|
|
303
312
|
}
|
|
304
313
|
return !0;
|
|
305
314
|
} catch (error) {
|
|
306
|
-
console.error(`${colors.red}Error processing files:${colors.reset}`, error);
|
|
315
|
+
console.error(`${colors.red}Error processing files:${colors.reset}`, formatBuildFailure(error));
|
|
307
316
|
return !1;
|
|
308
317
|
}
|
|
309
318
|
};
|
|
@@ -494,7 +503,7 @@ export async function serveMultipleStxFiles(filePaths, options = {}) {
|
|
|
494
503
|
if (isRouterRequest) {
|
|
495
504
|
const mainMatch = content.match(/<main[^>]*>([\s\S]*?)<\/main>/i);
|
|
496
505
|
if (mainMatch) {
|
|
497
|
-
const titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i), partialHtml = `<!DOCTYPE html><html><head><title>${
|
|
506
|
+
const titleMatch = content.match(/<title[^>]*>([\s\S]*?)<\/title>/i), title = titleMatch ? titleMatch[1] : "", partialHtml = `<!DOCTYPE html><html><head><title>${escapeTitle(title)}</title></head><body><main>${mainMatch[1]}</main></body></html>`;
|
|
498
507
|
return new Response(partialHtml, {
|
|
499
508
|
headers: {
|
|
500
509
|
"Content-Type": "text/html",
|
|
@@ -579,3 +588,6 @@ Press ${colors.cyan}h${colors.reset} + ${colors.cyan}Enter${colors.reset} to sho
|
|
|
579
588
|
}
|
|
580
589
|
return !0;
|
|
581
590
|
}
|
|
591
|
+
function escapeTitle(value) {
|
|
592
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
593
|
+
}
|
package/dist/document-shell.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import type { AppHeadConfig } from './types/config-types';
|
|
2
2
|
import type { ColorModeBootConfig } from './color-mode-boot';
|
|
3
|
+
/** Emit `k="v"` pairs with every value attribute-escaped. */
|
|
4
|
+
/**
|
|
5
|
+
* The identity of a `<meta>` tag, for deduplication.
|
|
6
|
+
*
|
|
7
|
+
* Shared by both places that merge config head tags into a page, because they
|
|
8
|
+
* disagreed: injectConfigHeadTags deduped and generateDocumentShell just
|
|
9
|
+
* concatenated, so the same `app.head.meta` produced one `<meta charset>` on a
|
|
10
|
+
* page that owned its document and two on a page built through the shell
|
|
11
|
+
* (stacksjs/stx#1840). One key, one answer.
|
|
12
|
+
*
|
|
13
|
+
* Returns '' for a tag with no identifying attribute, which the callers treat
|
|
14
|
+
* as "cannot dedup, keep it".
|
|
15
|
+
*/
|
|
16
|
+
export declare function metaDedupKey(m: Record<string, string>): string;
|
|
3
17
|
/**
|
|
4
18
|
* Build the attribute string for the shell's `<html>` open tag.
|
|
5
19
|
*
|
|
@@ -72,6 +86,19 @@ export declare function hoistLeadingHeadTags(content: string): { body: string, h
|
|
|
72
86
|
export declare function generateDocumentShell(content: string, headConfig?: AppHeadConfig, options?: {
|
|
73
87
|
/** Page-specific title (overrides config) */
|
|
74
88
|
title?: string
|
|
89
|
+
/**
|
|
90
|
+
* The title is already HTML and must NOT be escaped again.
|
|
91
|
+
*
|
|
92
|
+
* Set when the title was lifted out of author-written markup — an
|
|
93
|
+
* `@head <title>` block or `@section('title')` — where entities are already
|
|
94
|
+
* in their final form and any interpolation has already been escaped by
|
|
95
|
+
* `{{ }}`. Escaping it a second time renders `Café & Bar` as
|
|
96
|
+
* `Caf&eacute; &amp; Bar`.
|
|
97
|
+
*
|
|
98
|
+
* A title from `useHead({ title })` is a plain JS string and must stay
|
|
99
|
+
* escaped — that is the XSS path (#1792 item 4).
|
|
100
|
+
*/
|
|
101
|
+
titleIsHtml?: boolean
|
|
75
102
|
/** Extra styles to inject in <head> (e.g. Crosswind CSS, scoped styles) */
|
|
76
103
|
styles?: string[]
|
|
77
104
|
/** Extra scripts to inject before </body> (e.g. signals runtime, router) */
|
|
@@ -104,6 +131,44 @@ export declare function generateDocumentShell(content: string, headConfig?: AppH
|
|
|
104
131
|
* which made the check spuriously fail and auto-shell wrap the user's
|
|
105
132
|
* complete HTML doc as if it were just body content.
|
|
106
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Does this string OPEN a full HTML document?
|
|
136
|
+
*
|
|
137
|
+
* Anchored, but tolerant of what stx's own pipeline prepends: whitespace, HTML
|
|
138
|
+
* comments, and `<script>` blocks (the signals runtime, scoped setup scripts,
|
|
139
|
+
* theme guards) all routinely land ahead of a layout's markup.
|
|
140
|
+
*
|
|
141
|
+
* The check used to be `/<!DOCTYPE\b/i` — unanchored — so the literal text
|
|
142
|
+
* `<!DOCTYPE` ANYWHERE made a fragment look like a document and sent it through
|
|
143
|
+
* wrapper-stripping. Easy to hit on any page that documents HTML: a doctype in
|
|
144
|
+
* a comment, in a `<pre><code>` sample, or in a JS string (stacksjs#1787). The
|
|
145
|
+
* `<html>` half was already anchored, so the asymmetry was clearly accidental.
|
|
146
|
+
*
|
|
147
|
+
* Note this is the opposite bias from `hasDocumentShell` in document-shell.ts,
|
|
148
|
+
* which stays permissive on purpose: there, a false negative double-wraps a
|
|
149
|
+
* complete document, so leaning towards "yes, it's a document" is the safe
|
|
150
|
+
* direction. Here a false positive shreds a fragment, so it leans the other way.
|
|
151
|
+
*/
|
|
152
|
+
export declare function startsDocument(html: string): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Does this output already carry a document wrapper?
|
|
155
|
+
*
|
|
156
|
+
* ANCHORED, and it has to be. The test used to be an unanchored
|
|
157
|
+
* `/<html[\s>]/i.test(html)`, so a DOCTYPE or an html tag mentioned ANYWHERE —
|
|
158
|
+
* including inside an HTML comment — reported a shell that was not there
|
|
159
|
+
* (stacksjs/stx#1792, part one item 3).
|
|
160
|
+
*
|
|
161
|
+
* The cascade was silent and severe: no shell means no `<head>`, so
|
|
162
|
+
* `injectCloakStyle` bails and every element stx deliberately cloaked renders
|
|
163
|
+
* VISIBLE; no `<meta name="stx-layout">`; and with neither that meta nor
|
|
164
|
+
* `data-stx-content` the router's document sniff fails, so every navigation
|
|
165
|
+
* hard-reloads. One comment mentioning the root element was enough, and the
|
|
166
|
+
* reporter ended up writing a house rule telling authors not to name the tag in
|
|
167
|
+
* prose.
|
|
168
|
+
*
|
|
169
|
+
* Shares `startsDocument` with the app-shell path, which was anchored for the
|
|
170
|
+
* same reason in #1787 and had been the outlier's correct twin ever since.
|
|
171
|
+
*/
|
|
107
172
|
export declare function hasDocumentShell(html: string): boolean;
|
|
108
173
|
/**
|
|
109
174
|
* Inject the config `app.head` tags — script / meta / link / headRaw, but NOT
|
package/dist/document-shell.js
CHANGED
|
@@ -4,6 +4,23 @@ const SAFE_ATTR_NAME = /^[A-Za-z_:][\w:.-]*$/;
|
|
|
4
4
|
function escapeAttr(value) {
|
|
5
5
|
return String(value).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
6
6
|
}
|
|
7
|
+
function escapeText(value) {
|
|
8
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
9
|
+
}
|
|
10
|
+
export function metaDedupKey(m) {
|
|
11
|
+
if (m.name)
|
|
12
|
+
return `name="${m.name}"`;
|
|
13
|
+
if (m.property)
|
|
14
|
+
return `property="${m.property}"`;
|
|
15
|
+
if (m.charset != null)
|
|
16
|
+
return "charset=";
|
|
17
|
+
if (m["http-equiv"])
|
|
18
|
+
return `http-equiv="${m["http-equiv"]}"`;
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
function attrPairs(entries, skip) {
|
|
22
|
+
return Object.entries(entries).filter(([k]) => !skip?.(k)).map(([k, v]) => `${k}="${escapeAttr(String(v))}"`).join(" ");
|
|
23
|
+
}
|
|
7
24
|
export function buildHtmlAttrs(htmlAttrs = {}, lang = "en") {
|
|
8
25
|
const rest = [], managed = [];
|
|
9
26
|
let className = "", resolvedLang = lang;
|
|
@@ -188,26 +205,29 @@ export function generateDocumentShell(content, headConfig = {}, options = {}) {
|
|
|
188
205
|
htmlAttrs = {},
|
|
189
206
|
bodyClass = "",
|
|
190
207
|
bodyAttrs = {}
|
|
191
|
-
} = headConfig, pageTitle = options.title || configTitle,
|
|
208
|
+
} = headConfig, pageTitle = options.title || configTitle, defaultMeta = [
|
|
192
209
|
{ charset: "UTF-8" },
|
|
193
210
|
{ name: "viewport", content: "width=device-width, initial-scale=1.0" }
|
|
194
|
-
],
|
|
195
|
-
|
|
211
|
+
], configuredMetaKeys = new Set(meta.map(metaDedupKey).filter(Boolean)), metaTags = [
|
|
212
|
+
...defaultMeta.filter((d) => !configuredMetaKeys.has(metaDedupKey(d))),
|
|
213
|
+
...meta
|
|
214
|
+
].map((m) => {
|
|
215
|
+
return ` <meta ${attrPairs(m)}>`;
|
|
196
216
|
}).join(`
|
|
197
217
|
`), linkTags = link.map((l) => {
|
|
198
|
-
return ` <link ${
|
|
218
|
+
return ` <link ${attrPairs(l)}>`;
|
|
199
219
|
}).join(`
|
|
200
220
|
`), configHeadScripts = headScriptTags.map((s) => {
|
|
201
221
|
if (s.src)
|
|
202
|
-
return ` <script ${
|
|
222
|
+
return ` <script ${attrPairs(s, (k) => k === "content")}></script>`;
|
|
203
223
|
if (s.content)
|
|
204
224
|
return ` <script>${s.content}</script>`;
|
|
205
225
|
return "";
|
|
206
226
|
}).filter(Boolean).join(`
|
|
207
|
-
`), htmlAttrStr = buildHtmlAttrs(htmlAttrs, lang), bodyAttrStr = Object.entries(bodyAttrs).map(([k, v]) => ` ${k}="${v}"`).join(""), bodyClassStr = bodyClass ? ` class="${bodyClass}"` : "", { body: pageBody, hoisted: pageHeadTags } = hoistLeadingHeadTags(content), headParts = [
|
|
227
|
+
`), htmlAttrStr = buildHtmlAttrs(htmlAttrs, lang), bodyAttrStr = Object.entries(bodyAttrs).map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`).join(""), bodyClassStr = bodyClass ? ` class="${escapeAttr(bodyClass)}"` : "", { body: pageBody, hoisted: pageHeadTags } = hoistLeadingHeadTags(content), headParts = [
|
|
208
228
|
metaTags,
|
|
209
229
|
options.colorMode ? ` ${generateColorModeBootScript(options.colorMode)}` : "",
|
|
210
|
-
` <title>${pageTitle}</title>`,
|
|
230
|
+
` <title>${options.titleIsHtml ? pageTitle : escapeText(pageTitle)}</title>`,
|
|
211
231
|
` ${CLOAK_STYLE}`,
|
|
212
232
|
linkTags,
|
|
213
233
|
configHeadScripts,
|
|
@@ -233,8 +253,35 @@ ${bodyParts.join(`
|
|
|
233
253
|
</body>
|
|
234
254
|
</html>`;
|
|
235
255
|
}
|
|
256
|
+
export function startsDocument(html) {
|
|
257
|
+
let i = 0;
|
|
258
|
+
while (i < html.length) {
|
|
259
|
+
const rest = html.slice(i), ws = rest.match(/^\s+/);
|
|
260
|
+
if (ws) {
|
|
261
|
+
i += ws[0].length;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (rest.startsWith("<!--")) {
|
|
265
|
+
const end = html.indexOf("-->", i);
|
|
266
|
+
if (end === -1)
|
|
267
|
+
return !1;
|
|
268
|
+
i = end + 3;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const preamble = rest.match(/^<(script|style)\b[^>]*>/i);
|
|
272
|
+
if (preamble) {
|
|
273
|
+
const closeTag = `</${preamble[1].toLowerCase()}>`, end = html.toLowerCase().indexOf(closeTag, i + preamble[0].length);
|
|
274
|
+
if (end === -1)
|
|
275
|
+
return !1;
|
|
276
|
+
i = end + closeTag.length;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
return /^<!DOCTYPE\b/i.test(rest) || /^<html[\s>]/i.test(rest);
|
|
280
|
+
}
|
|
281
|
+
return !1;
|
|
282
|
+
}
|
|
236
283
|
export function hasDocumentShell(html) {
|
|
237
|
-
return
|
|
284
|
+
return startsDocument(html);
|
|
238
285
|
}
|
|
239
286
|
export function injectConfigHeadTags(html, headConfig = {}) {
|
|
240
287
|
const headCloseIdx = html.lastIndexOf("</head>");
|
|
@@ -242,21 +289,21 @@ export function injectConfigHeadTags(html, headConfig = {}) {
|
|
|
242
289
|
return html;
|
|
243
290
|
const { meta = [], link = [], script = [], headRaw = "" } = headConfig, head = html.slice(0, headCloseIdx), parts = [];
|
|
244
291
|
for (const m of meta) {
|
|
245
|
-
const dedup = m
|
|
292
|
+
const dedup = metaDedupKey(m);
|
|
246
293
|
if (dedup && head.includes(dedup))
|
|
247
294
|
continue;
|
|
248
|
-
parts.push(` <meta ${
|
|
295
|
+
parts.push(` <meta ${attrPairs(m)}>`);
|
|
249
296
|
}
|
|
250
297
|
for (const l of link) {
|
|
251
298
|
if (l.href && head.includes(`href="${l.href}"`))
|
|
252
299
|
continue;
|
|
253
|
-
parts.push(` <link ${
|
|
300
|
+
parts.push(` <link ${attrPairs(l)}>`);
|
|
254
301
|
}
|
|
255
302
|
for (const s of script)
|
|
256
303
|
if (s.src) {
|
|
257
304
|
if (head.includes(`src="${s.src}"`))
|
|
258
305
|
continue;
|
|
259
|
-
parts.push(` <script ${
|
|
306
|
+
parts.push(` <script ${attrPairs(s, (k) => k === "content")}></script>`);
|
|
260
307
|
} else if (s.content) {
|
|
261
308
|
if (head.includes(s.content))
|
|
262
309
|
continue;
|
|
@@ -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
|
// =============================================================================
|