@stacksjs/stx 0.2.113 → 0.2.114

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/render.js CHANGED
@@ -3,32 +3,12 @@ import { defaultConfig } from "./config.js";
3
3
  import { extractVariables } from "./variable-extractor.js";
4
4
  import { injectRouterScript, processDirectives } from "./process.js";
5
5
  import { extractBridgeData, processClientScript } from "./client-script.js";
6
+ import { findSfcTemplateBlock } from "./sfc-template.js";
6
7
  function parseTemplate(content) {
7
8
  let workingContent = content;
8
- const templateOpenMatch = content.match(/<template\b(?![^>]*(?:\b(?:id|x-for|x-if|@for|@if|:for|:if)\s*=|\s#[\w-]|\bv-slot|\bslot\s*=))[^>]*>/i);
9
- if (templateOpenMatch && templateOpenMatch.index !== void 0) {
10
- const openEnd = templateOpenMatch.index + templateOpenMatch[0].length;
11
- let depth = 1, pos = openEnd;
12
- while (depth > 0 && pos < content.length) {
13
- const nextOpen = content.indexOf("<template", pos), nextClose = content.indexOf("</template>", pos);
14
- if (nextClose === -1)
15
- break;
16
- if (nextOpen !== -1 && nextOpen < nextClose) {
17
- const after = content[nextOpen + 9];
18
- if (after === ">" || after === " " || after === `
19
- ` || after === "\t")
20
- depth++;
21
- pos = nextOpen + 9;
22
- } else {
23
- depth--;
24
- if (depth === 0) {
25
- workingContent = content.slice(openEnd, nextClose).trim();
26
- break;
27
- }
28
- pos = nextClose + 11;
29
- }
30
- }
31
- }
9
+ const templateBlock = findSfcTemplateBlock(content);
10
+ if (templateBlock)
11
+ workingContent = templateBlock.content.trim();
32
12
  const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi, clientScripts = [], serverScripts = [], signalsScripts = [], usesSignalsAPI = (scriptContent) => /\b(?:state|derived|effect|batch)\s*(?:<[^<>()]*>)?\s*\(/.test(scriptContent);
33
13
  let scriptMatch;
34
14
  while ((scriptMatch = scriptRegex.exec(content)) !== null) {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Build the destructuring statement that pulls {@link STX_RUNTIME_GLOBALS} into
3
+ * a generated client script's scope.
4
+ *
5
+ * @param declaration `var` for scripts that reassign a binding (the `@include`
6
+ * wrapper rebinds `onDestroy` to tee into its teardown channel — an assignment,
7
+ * which `var` permits), `const` otherwise.
8
+ * @param exclude names the call site declares itself; see
9
+ * {@link COMPONENT_SCOPE_LOCAL_GLOBALS}.
10
+ */
11
+ export declare function buildRuntimeGlobalsDestructure(declaration?: 'const' | 'var', exclude?: readonly string[]): string;
12
+ /**
13
+ * Runtime Globals
14
+ *
15
+ * The single source of truth for the stx APIs destructured off `window.stx` at
16
+ * the top of every generated client script, so user code can write `state(...)`
17
+ * or `onMount(...)` without importing anything.
18
+ *
19
+ * This list used to be hardcoded separately in three places — the `@include`
20
+ * component wrapper, the page setup function, and the `<Component />` client
21
+ * script wrapper. They drifted, so which APIs existed depended on which kind of
22
+ * script you happened to be writing, and the failure was a bare `ReferenceError`
23
+ * at runtime with nothing at build time: `nextTick()` resolved in a component
24
+ * script but threw in a page, `onMounted()` resolved in a page but threw in an
25
+ * `@include`d component. See #1785.
26
+ *
27
+ * Two entries were also phantoms — `inject` and `nextTick` were destructured by
28
+ * one site but never defined on `window.stx` at all, so they silently evaluated
29
+ * to `undefined` and threw "is not a function" when called. They are omitted
30
+ * here; re-add them only alongside a real implementation.
31
+ *
32
+ * Adding a runtime global is now a one-line change here. `runtime-globals.test.ts`
33
+ * asserts every name actually exists on `window.stx` and that all three call
34
+ * sites emit the same set, which is what keeps this from drifting again.
35
+ *
36
+ * @module runtime-globals
37
+ */
38
+ /**
39
+ * Public stx APIs made available as bare identifiers inside client scripts.
40
+ *
41
+ * Sorted for a stable diff. Internal `window.stx` members (`_scopes`,
42
+ * `_cleanupContainer`, …) are deliberately excluded — they are runtime
43
+ * plumbing, not part of the authoring surface.
44
+ */
45
+ export declare const STX_RUNTIME_GLOBALS: readonly string[];
46
+ /**
47
+ * Names a call site declares itself and therefore must NOT also destructure.
48
+ *
49
+ * The `<Component />` wrapper defines scope-aware `onMount`/`onDestroy` that
50
+ * push into that component's own callback arrays:
51
+ *
52
+ * const onMount = (fn) => __scopeVars.__mountCallbacks.push(fn)
53
+ *
54
+ * Destructuring the same names first makes that a duplicate `const` — a
55
+ * SyntaxError, which surfaces as the whole chunk silently failing to minify (and
56
+ * failing to run). This omission is deliberate, unlike the accidental drift
57
+ * #1785 fixed, so it is declared here rather than left implicit.
58
+ */
59
+ export declare const COMPONENT_SCOPE_LOCAL_GLOBALS: readonly string[];
@@ -0,0 +1,65 @@
1
+ export const STX_RUNTIME_GLOBALS = [
2
+ "batch",
3
+ "computed",
4
+ "defineEmits",
5
+ "defineExpose",
6
+ "definePageMeta",
7
+ "defineProps",
8
+ "defineSlots",
9
+ "defineStore",
10
+ "derived",
11
+ "effect",
12
+ "goBack",
13
+ "goForward",
14
+ "navigate",
15
+ "onBeforeUnmount",
16
+ "onDestroy",
17
+ "onMount",
18
+ "onMounted",
19
+ "onUnmounted",
20
+ "provide",
21
+ "reactive",
22
+ "ref",
23
+ "registerStoresClient",
24
+ "state",
25
+ "useAsync",
26
+ "useClickOutside",
27
+ "useColorMode",
28
+ "useCookie",
29
+ "useCounter",
30
+ "useDark",
31
+ "useDebounce",
32
+ "useDebouncedValue",
33
+ "useEventListener",
34
+ "useFetch",
35
+ "useFocus",
36
+ "useHead",
37
+ "useInterval",
38
+ "useLocalStorage",
39
+ "useMediaQuery",
40
+ "useMutation",
41
+ "useOptimistic",
42
+ "usePreferredContrast",
43
+ "usePreferredDark",
44
+ "usePreferredLight",
45
+ "usePreferredReducedMotion",
46
+ "useQuery",
47
+ "useReactiveProp",
48
+ "useRef",
49
+ "useRoute",
50
+ "useSearchParams",
51
+ "useSeoMeta",
52
+ "useSessionStorage",
53
+ "useStore",
54
+ "useThrottle",
55
+ "useTimeout",
56
+ "useToggle",
57
+ "useWebSocket",
58
+ "watch",
59
+ "watchEffect",
60
+ "withDefaults"
61
+ ], COMPONENT_SCOPE_LOCAL_GLOBALS = ["onMount", "onDestroy"];
62
+ export function buildRuntimeGlobalsDestructure(declaration = "const", exclude = []) {
63
+ const names = exclude.length ? STX_RUNTIME_GLOBALS.filter((name) => !exclude.includes(name)) : STX_RUNTIME_GLOBALS;
64
+ return `${declaration} { ${names.join(", ")} } = window.stx;`;
65
+ }
@@ -50,7 +50,7 @@ export async function injectSignalsRuntime(template, options) {
50
50
  if (template.includes("window.stx.state") || template.includes("STX Signals Runtime"))
51
51
  return template;
52
52
  if (options.buildMode === "compile") {
53
- const runtimeScript = '<script src="/__stx/runtime.__STX_HASH__.js"></script>', firstScriptInDoc = template.indexOf("<script");
53
+ const runtimeScript = '<script data-stx-runtime src="/__stx/runtime.__STX_HASH__.js"></script>', firstScriptInDoc = template.indexOf("<script");
54
54
  if (firstScriptInDoc !== -1)
55
55
  return template.slice(0, firstScriptInDoc) + runtimeScript + `
56
56
  ` + template.slice(firstScriptInDoc);
@@ -62,7 +62,7 @@ export async function injectSignalsRuntime(template, options) {
62
62
  return runtimeScript + `
63
63
  ` + template;
64
64
  }
65
- const { getCachedSignalsRuntime } = await import("./caching.js"), runtimeScript = `<script data-stx-scoped>${await getCachedSignalsRuntime(options.debug)}</script>`, firstScriptInDoc = template.indexOf("<script");
65
+ const { getCachedSignalsRuntime } = await import("./caching.js"), runtimeScript = `<script data-stx-scoped data-stx-runtime>${await getCachedSignalsRuntime(options.debug)}</script>`, firstScriptInDoc = template.indexOf("<script");
66
66
  if (firstScriptInDoc !== -1)
67
67
  return template.slice(0, firstScriptInDoc) + runtimeScript + `
68
68
  ` + template.slice(firstScriptInDoc);
package/dist/serve.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { serve as bunServe } from "bun";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { findSfcTemplateBlock } from "./sfc-template.js";
4
5
  import { readMarkdownFile } from "./assets.js";
5
6
  import { processDirectives } from "./process.js";
6
7
  import { extractVariables } from "./utils.js";
@@ -33,27 +34,9 @@ export async function serve(options = {}) {
33
34
  if (key.startsWith(`${filePath}:`) && key !== cacheKey)
34
35
  fileCache.delete(key);
35
36
  let content = await Bun.file(filePath).text(), workingContent = content;
36
- const templateOpenMatch = content.match(/<template\b(?![^>]*(?:\b(?:id|x-for|x-if|@for|@if|:for|:if)\s*=|\s#[\w-]|\bv-slot|\bslot\s*=))[^>]*>/i);
37
- if (templateOpenMatch && templateOpenMatch.index !== void 0) {
38
- const contentStart = templateOpenMatch.index + templateOpenMatch[0].length;
39
- let depth = 1, pos = contentStart;
40
- while (pos < content.length && depth > 0) {
41
- const openIdx = content.indexOf("<template", pos), closeIdx = content.indexOf("</template>", pos);
42
- if (closeIdx === -1)
43
- break;
44
- if (openIdx !== -1 && openIdx < closeIdx) {
45
- depth++;
46
- pos = openIdx + 9;
47
- } else {
48
- depth--;
49
- if (depth === 0) {
50
- workingContent = content.substring(contentStart, closeIdx).trim();
51
- break;
52
- }
53
- pos = closeIdx + 11;
54
- }
55
- }
56
- }
37
+ const templateBlock = findSfcTemplateBlock(content);
38
+ if (templateBlock)
39
+ workingContent = templateBlock.content.trim();
57
40
  const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi, clientScripts = [], serverScripts = [];
58
41
  let scriptMatch;
59
42
  while ((scriptMatch = scriptRegex.exec(content)) !== null) {
@@ -0,0 +1,17 @@
1
+ export declare function isClientTemplateTag(openingTag: string): boolean;
2
+ export declare function findSfcTemplateBlock(source: string): SfcTemplateBlock | null;
3
+ /**
4
+ * Locate the explicit wrapper `<template>` in an STX single-file component.
5
+ *
6
+ * A bare `<template>` can be an SFC wrapper, while a template carrying a
7
+ * client directive (`:for`, `@if`, `x-else`, slots, or an id) is runtime DOM
8
+ * and must stay in the rendered body. Keeping this distinction in one scanner
9
+ * avoids subtle regex boundary bugs for punctuation-prefixed attributes.
10
+ */
11
+ export declare interface SfcTemplateBlock {
12
+ start: number
13
+ openEnd: number
14
+ closeStart: number
15
+ end: number
16
+ content: string
17
+ }
@@ -0,0 +1,32 @@
1
+ const TEMPLATE_TAG_RE = /<\/?template\b[^>]*>/gi, CLIENT_TEMPLATE_ATTRIBUTE_RE = /(?:^|\s)(?:id\s*=|(?:x-|[@:])(?:for|if|else(?:-if)?)(?=\s|=|$)|#[\w-]+(?:\s*=|\s|$)|v-slot(?::[\w-]+)?(?=\s|=|$)|slot\s*=)/i;
2
+ export function isClientTemplateTag(openingTag) {
3
+ const attrs = openingTag.replace(/^<template\b/i, "").replace(/>$/, "");
4
+ return CLIENT_TEMPLATE_ATTRIBUTE_RE.test(attrs);
5
+ }
6
+ export function findSfcTemplateBlock(source) {
7
+ TEMPLATE_TAG_RE.lastIndex = 0;
8
+ let opening;
9
+ while ((opening = TEMPLATE_TAG_RE.exec(source)) !== null) {
10
+ if (opening[0].startsWith("</") || isClientTemplateTag(opening[0]))
11
+ continue;
12
+ const start = opening.index, openEnd = TEMPLATE_TAG_RE.lastIndex;
13
+ let depth = 1, nested;
14
+ while ((nested = TEMPLATE_TAG_RE.exec(source)) !== null)
15
+ if (nested[0].startsWith("</")) {
16
+ depth--;
17
+ if (depth === 0) {
18
+ const closeStart = nested.index;
19
+ return {
20
+ start,
21
+ openEnd,
22
+ closeStart,
23
+ end: TEMPLATE_TAG_RE.lastIndex,
24
+ content: source.slice(openEnd, closeStart)
25
+ };
26
+ }
27
+ } else
28
+ depth++;
29
+ return null;
30
+ }
31
+ return null;
32
+ }
@@ -1,21 +1,33 @@
1
1
  import { generateServerDataBridge } from "./client-script.js";
2
2
  import { findIfBlocks } from "./parser/index.js";
3
+ import { buildRuntimeGlobalsDestructure } from "./runtime-globals.js";
3
4
  import { transformStoreImports } from "./store-imports.js";
4
5
  import { shouldTranspileTypeScript, transpileTypeScript } from "./utils.js";
5
6
  import { injectSignalsRuntime, injectBrowserRuntime } from "./runtime-injection.js";
6
7
  let signalSetupCounter = 0;
7
- const SCRIPT_OPEN_RE = /<script\b([^>]*)>/gi, SINGLE_ELEMENT_RE = /^<([a-zA-Z][a-zA-Z0-9-]*)\b((?:\s+[^=\s>]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*\s*)>([\s\S]*)<\/\1>$/s;
8
+ const TAG_ATTR_RUN = `(?:"[^"]*"|'[^']*'|[^>"'])*`, SCRIPT_OPEN_RE = /<script\b([^>]*)>/gi, SINGLE_ELEMENT_RE = /^<([a-zA-Z][a-zA-Z0-9-]*)\b((?:\s+[^=\s>]+(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*\s*)>([\s\S]*)<\/\1>$/s;
8
9
  export function scanScriptTags(html, opts = {}) {
9
10
  const out = [];
10
- SCRIPT_OPEN_RE.lastIndex = 0;
11
- let m;
12
- while ((m = SCRIPT_OPEN_RE.exec(html)) !== null) {
11
+ let cursor = 0;
12
+ while (cursor < html.length) {
13
+ SCRIPT_OPEN_RE.lastIndex = cursor;
14
+ const m = SCRIPT_OPEN_RE.exec(html);
15
+ if (!m)
16
+ break;
17
+ const commentStart = html.indexOf("<!--", cursor);
18
+ if (commentStart !== -1 && commentStart < m.index) {
19
+ const commentEnd = html.indexOf("-->", commentStart + 4);
20
+ if (commentEnd === -1)
21
+ break;
22
+ cursor = commentEnd + 3;
23
+ continue;
24
+ }
13
25
  const attrs = m[1], bodyStart = m.index + m[0].length, closeIdx = html.toLowerCase().indexOf("</script>", bodyStart);
14
26
  if (closeIdx === -1)
15
27
  break;
16
28
  const end = closeIdx + 9;
17
29
  if (opts.skipAttrs && opts.skipAttrs.test(attrs)) {
18
- SCRIPT_OPEN_RE.lastIndex = end;
30
+ cursor = end;
19
31
  continue;
20
32
  }
21
33
  out.push({
@@ -25,7 +37,7 @@ export function scanScriptTags(html, opts = {}) {
25
37
  start: m.index,
26
38
  end
27
39
  });
28
- SCRIPT_OPEN_RE.lastIndex = end;
40
+ cursor = end;
29
41
  }
30
42
  return out;
31
43
  }
@@ -77,6 +89,8 @@ export function hasSignalsSyntax(template) {
77
89
  /\breactive\s*(?:<[^>]*>)?\s*\(/,
78
90
  /\bwatch\s*(?:<[^>]*>)?\s*\(/,
79
91
  /\bwatchEffect\s*(?:<[^>]*>)?\s*\(/,
92
+ /\buseReactiveProp\s*(?:<[^>]*>)?\s*\(/,
93
+ /\s:(?:if|else-if|else|for|show|text|html|model)\s*=/,
80
94
  /data-stx(?:-auto)?(?![-\w])/,
81
95
  /data-stx-scoped/
82
96
  ].some((pattern) => pattern.test(template));
@@ -654,7 +668,7 @@ function dedupeTopLevelDeclarations(code) {
654
668
  export async function processScriptSetup(template, filePath, serverData) {
655
669
  const scripts = scanScriptTags(template, {
656
670
  skipAttrs: /\bserver\b|\bsrc\s*=|\bdata-stx-scoped\b|\bdata-stx-router\b/
657
- }), SIGNAL_API_RE = /\b(?:state|derived|effect|ref|reactive|computed|watch|watchEffect|useStore|useLocalStorage|useSessionStorage|useCookie|useReactiveProp|useFetch|useRef|useEventListener|useDebounce|useDebouncedValue|useThrottle|useInterval|useTimeout|useToggle|useCounter|useClickOutside|useFocus|useAsync|useColorMode|useDark|useWebSocket|useRoute|useSearchParams|onMount|onDestroy|defineProps|withDefaults|defineEmits|defineExpose|defineSlots|useOptimistic)\s*(?:<[^>]*>)?\s*\(/, signalScripts = [];
671
+ }), SIGNAL_API_RE = /\b(?:state|derived|effect|ref|reactive|computed|watch|watchEffect|useStore|useLocalStorage|useSessionStorage|useCookie|useReactiveProp|useFetch|useRef|useEventListener|useDebounce|useDebouncedValue|useThrottle|useInterval|useTimeout|useToggle|useCounter|useClickOutside|useFocus|useAsync|useColorMode|useDark|useMediaQuery|usePreferredDark|usePreferredLight|usePreferredReducedMotion|usePreferredContrast|useWebSocket|useRoute|useSearchParams|onMount|onDestroy|defineProps|withDefaults|defineEmits|defineExpose|defineSlots|useOptimistic)\s*(?:<[^>]*>)?\s*\(/, signalScripts = [];
658
672
  for (const s of scripts) {
659
673
  let content = s.body;
660
674
  if (shouldTranspileTypeScript(s.attrs))
@@ -685,21 +699,22 @@ ${resolved}`);
685
699
  const mergedExports = extractExports(mergedContent), bridge = generateServerDataBridge(mergedContent, serverData), setupCode = `
686
700
  <script data-stx-scoped>
687
701
  function ${setupFnName}() {
688
- const { state, derived, effect, batch, onMount, onDestroy, defineStore, useStore, useFetch, useRef, useQuery, useMutation, useOptimistic, useDebounce, useDebouncedValue, useThrottle, useInterval, useTimeout, useToggle, useCounter, useClickOutside, useFocus, useAsync, useLocalStorage, useSessionStorage, useCookie, useReactiveProp, useEventListener, useWebSocket, useColorMode, useDark, useHead, useSeoMeta, definePageMeta, useRoute, useSearchParams, navigate, goBack, goForward, provide, ref, reactive, computed, watch, watchEffect, defineProps, withDefaults, defineEmits, defineExpose, defineSlots } = window.stx;
702
+ ${buildRuntimeGlobalsDestructure("const")}
689
703
  ${bridge}${mergedContent}
690
704
  return { ${mergedExports} };
691
705
  }
706
+ window.__stx_latestSetup=${setupFnName};
692
707
  if(window.stx)window.stx._latestSetup=${setupFnName};
693
708
  </script>`;
694
709
  let output = template;
695
710
  for (const s of signalScripts)
696
711
  output = output.split(s.fullMatch).join("");
697
712
  if (output.includes("<body"))
698
- output = output.replace(/<body([^>]*)>/, `<body$1 data-stx="${setupFnName}">`);
713
+ output = output.replace(new RegExp(`<body(${TAG_ATTR_RUN})>`), `<body$1 data-stx="${setupFnName}">`);
699
714
  else {
700
715
  const skipTags = ["script", "style", "html", "head", "meta", "link", "title", "!doctype"];
701
716
  let marked = !1;
702
- output = output.replace(/<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*)>/gi, (match, tag, attrs) => {
717
+ output = output.replace(new RegExp(`<([a-zA-Z][a-zA-Z0-9-]*)\\b(${TAG_ATTR_RUN})>`, "gi"), (match, tag, attrs) => {
703
718
  if (marked || skipTags.includes(tag.toLowerCase()))
704
719
  return match;
705
720
  marked = !0;
@@ -955,7 +970,7 @@ export function findMarkupIndexOutsideScripts(html, pattern) {
955
970
  }
956
971
  return -1;
957
972
  }
958
- const STX_IMPORT_STRIPPED = "// [stx import stripped \u2014 resolved via window.stx in __stx_setup]";
973
+ const STX_IMPORT_STRIPPED = "/* [stx import stripped, resolved via window.stx in __stx_setup] */";
959
974
  export function rewriteStxImportSpecifiers(specifiers) {
960
975
  const aliases = [];
961
976
  for (const raw of specifiers.split(",")) {
@@ -968,5 +983,5 @@ export function rewriteStxImportSpecifiers(specifiers) {
968
983
  }
969
984
  if (aliases.length === 0)
970
985
  return STX_IMPORT_STRIPPED;
971
- return `const ${aliases.join(", ")}; ${STX_IMPORT_STRIPPED}`;
986
+ return `var ${aliases.join(", ")}; ${STX_IMPORT_STRIPPED}`;
972
987
  }