@stacksjs/stx 0.2.113 → 0.2.115

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/stx.d.ts CHANGED
@@ -233,6 +233,14 @@ declare function useColorMode(): {
233
233
  setMode: (mode: 'light' | 'dark' | 'auto') => void
234
234
  }
235
235
  declare function useDark(): StxSignal<boolean>
236
+ declare function useMediaQuery(query: string): StxSignal<boolean> & {
237
+ readonly matches: boolean
238
+ readonly value: boolean
239
+ }
240
+ declare function usePreferredDark(): ReturnType<typeof useMediaQuery>
241
+ declare function usePreferredLight(): ReturnType<typeof useMediaQuery>
242
+ declare function usePreferredReducedMotion(): ReturnType<typeof useMediaQuery>
243
+ declare function usePreferredContrast(): ReturnType<typeof useMediaQuery>
236
244
 
237
245
  // ============================================================================
238
246
  // Head / SEO
package/dist/testing.js CHANGED
@@ -30,7 +30,14 @@ export function shimAttributes(root) {
30
30
  const m = el.__attrMap, arr = [];
31
31
  let i = 0;
32
32
  for (const [name, value] of m)
33
- arr[i++] = { name, value };
33
+ arr[i++] = {
34
+ name,
35
+ value,
36
+ *[Symbol.iterator]() {
37
+ yield name;
38
+ yield value;
39
+ }
40
+ };
34
41
  arr.length = i;
35
42
  arr.get = (k) => m.get(k);
36
43
  arr.set = (k, v) => m.set(k, v);
@@ -271,8 +271,10 @@ export declare interface StxConfig {
271
271
  enabled: boolean
272
272
  partialsDir: string
273
273
  componentsDir: string
274
+ _rootComponentsDir?: string
274
275
  layoutsDir?: string
275
276
  storesDir?: string
277
+ composablesDir?: string
276
278
  defaultLayout?: string
277
279
  plugins?: Array<string | [string, Record<string, any>]>
278
280
  publicDir?: string
package/dist/utils.d.ts CHANGED
@@ -45,6 +45,15 @@ export declare function isHydrateTrigger(trigger: string): boolean;
45
45
  * would flag components that have no slot expressions at all.
46
46
  */
47
47
  export declare function slotContentHasExpressions(slotContent: string): boolean;
48
+ /**
49
+ * Collect client variables whose initializer returns a signal-like value.
50
+ *
51
+ * Component scripts are removed before their template body reaches the
52
+ * expression pipeline. Recording these names in the component context keeps
53
+ * interpolations such as `{{ title() }}` client-owned even when a static prop
54
+ * named `title` also exists in the server context.
55
+ */
56
+ export declare function componentClientSignalNames(scripts: string[]): string[];
48
57
  export declare function renderComponentWithSlot(componentPath: string, props: Record<string, unknown>, slotContent: string, componentsDir: string, parentContext: Record<string, unknown>, parentFilePath: string, options: StxOptions, processedComponents?: Set<string> | undefined, dependencies: Set<string>): Promise<string>;
49
58
  /**
50
59
  * Check if a file exists
package/dist/utils.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { extractBridgeData, processClientScript } from "./client-script.js";
4
- import { processExpressions, unescapeHtml } from "./expressions.js";
3
+ import { extractAndStripCssImports, extractBridgeData, processClientScript, renderVendorStyleTags } from "./client-script.js";
4
+ import { interpolateScriptExpressions, processExpressions, unescapeHtml } from "./expressions.js";
5
+ import { COMPONENT_SCOPE_LOCAL_GLOBALS, buildRuntimeGlobalsDestructure } from "./runtime-globals.js";
5
6
  import { transformStoreImports } from "./store-imports.js";
6
7
  import { LRUCache } from "./performance-utils.js";
7
8
  import { processDirectives } from "./process.js";
8
9
  import { getPublicEnvDefine } from "./public-env.js";
9
10
  import { processScopedStyles } from "./style-scoping.js";
11
+ import { findSfcTemplateBlock } from "./sfc-template.js";
10
12
  export {
11
13
  extractDirectiveNames,
12
14
  getPositionInfo,
@@ -325,6 +327,7 @@ export async function userComponentFileExists(componentPath, componentsDir, pare
325
327
  if (originalFilePath)
326
328
  push(path.join(path.dirname(originalFilePath), "components"));
327
329
  push(componentsDir);
330
+ push(options._rootComponentsDir);
328
331
  push(path.join(path.dirname(parentFilePath), "components"));
329
332
  push(options.componentsDir);
330
333
  const pluginDirs = options._pluginComponentDirs;
@@ -356,6 +359,16 @@ export function slotContentHasExpressions(slotContent) {
356
359
  const withoutScripts = slotContent.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
357
360
  return /\{\{[\s\S]*?\}\}|\{!![\s\S]*?!!\}/.test(withoutScripts);
358
361
  }
362
+ export function componentClientSignalNames(scripts) {
363
+ const names = new Set, declaration = /\b(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:state|derived|computed|ref|reactive|signal|useReactiveProp|useLocalStorage|useSessionStorage|useCookie|useDebouncedValue)\s*(?:<[^>]*>)?\s*\(/g;
364
+ for (const script of scripts) {
365
+ let match;
366
+ while ((match = declaration.exec(script)) !== null)
367
+ names.add(match[1]);
368
+ declaration.lastIndex = 0;
369
+ }
370
+ return [...names];
371
+ }
359
372
  export async function renderComponentWithSlot(componentPath, props, slotContent, componentsDir, parentContext, parentFilePath, options, processedComponents = new Set, dependencies) {
360
373
  const { extractVariables } = await import("./variable-extractor.js"), components = processedComponents ?? new Set;
361
374
  if (components.has(componentPath))
@@ -396,6 +409,9 @@ export async function renderComponentWithSlot(componentPath, props, slotContent,
396
409
  }
397
410
  if (componentsDir)
398
411
  searchDirs.push(componentsDir);
412
+ const rootComponentsDir = options._rootComponentsDir;
413
+ if (typeof rootComponentsDir === "string" && !searchDirs.includes(rootComponentsDir))
414
+ searchDirs.push(rootComponentsDir);
399
415
  const parentRelative = path.join(path.dirname(parentFilePath), "components");
400
416
  if (!searchDirs.includes(parentRelative))
401
417
  searchDirs.push(parentRelative);
@@ -521,9 +537,9 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
521
537
  };
522
538
  applyDestructuredPropDefaults(componentContent, componentContext);
523
539
  let workingContent = componentContent;
524
- const templateMatch = workingContent.match(/<template\b(?![^>]*(?:\b(?:id|x-for|x-if|@for|@if|:for|:if)\s*=|\s#[\w-]|\bv-slot|\bslot\s*=))[^>]*>([\s\S]*?)<\/template>/i);
525
- if (templateMatch)
526
- workingContent = templateMatch[1].trim();
540
+ const templateBlock = findSfcTemplateBlock(workingContent);
541
+ if (templateBlock)
542
+ workingContent = templateBlock.content.trim();
527
543
  const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi, scriptMatches = [...componentContent.matchAll(scriptRegex)], clientScripts = [];
528
544
  for (const match of scriptMatches) {
529
545
  const attrs = match[1] || "";
@@ -537,8 +553,9 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
537
553
  } catch (e) {}
538
554
  if (!isServerScript) {
539
555
  let cleanContent = content.replace(/^\s*const\s*\{[^}]+\}\s*=\s*window\.stx\s*;?\s*\n?/gm, "");
540
- if (cleanContent.includes("{{") && cleanContent.includes("}}"))
541
- cleanContent = processExpressions(cleanContent, componentContext, componentFilePath);
556
+ const hasMustache = cleanContent.includes("{{") && cleanContent.includes("}}"), hasRawMarker = cleanContent.includes("{!!") && cleanContent.includes("!!}");
557
+ if (hasMustache || hasRawMarker)
558
+ cleanContent = interpolateScriptExpressions(cleanContent, componentContext);
542
559
  const cleanAttrs = attrs.replace(/\s*\bts\b/g, "").replace(/\s*\blang\s*=\s*["']?(ts|typescript)["']?/gi, "");
543
560
  clientScripts.push(`<script${cleanAttrs}>${cleanContent}</script>`);
544
561
  }
@@ -568,7 +585,7 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
568
585
  if (slotContent && templateContent.includes("{{ text || slot }}"))
569
586
  templateContent = templateContent.replace(/\{\{\s*text\s*\|\|\s*slot\s*\}\}/g, () => slotContent);
570
587
  const { applySlots } = await import("./slots.js"), { defaultSlot, namedSlots } = parsedSlotContent;
571
- templateContent = await applySlots(templateContent, defaultSlot || slotContent, namedSlots, componentContext);
588
+ templateContent = await applySlots(templateContent, defaultSlot, namedSlots, componentContext);
572
589
  for (const [key, value] of Object.entries(componentContext))
573
590
  if (typeof value === "string") {
574
591
  if (value.includes("<") && value.includes(">") || value.includes("&lt;") || value.includes("&quot;")) {
@@ -576,15 +593,24 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
576
593
  componentContext[key] = unescaped;
577
594
  }
578
595
  }
579
- const hasSignalScripts = clientScripts.some((s) => /\b(state|derived|effect)\s*(?:<[^<>()]*>)?\s*\(/.test(s)), componentOptions = {
596
+ const hasSignalScripts = clientScripts.some((s) => /\b(?:state|derived|effect|ref|reactive|computed|watch|watchEffect|useReactiveProp|defineProps|withDefaults|defineEmits|defineExpose|defineSlots)\s*(?:<[^<>()]*>)?\s*\(/.test(s)), clientSignalNames = componentClientSignalNames(clientScripts);
597
+ if (clientSignalNames.length > 0)
598
+ componentContext.__stx_client_signal_names = clientSignalNames;
599
+ const componentOptions = {
580
600
  ...options,
601
+ _rootComponentsDir: options._rootComponentsDir || options.componentsDir,
581
602
  componentsDir: path.dirname(componentFilePath),
582
603
  skipSignalsRuntime: !0,
583
- skipEventDirectives: hasSignalScripts,
604
+ skipEventDirectives: hasSignalScripts || clientScripts.length === 0,
584
605
  defaultLayout: void 0
585
606
  };
586
607
  componentContext.__stxProcessingDepth = (parentContext.__stxProcessingDepth || 0) + 1;
587
- const result = await processDirectives(templateContent, componentContext, componentFilePath, componentOptions, dependencies), scopeId = `stx_${baseName.replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}_${Math.random().toString(36).slice(2, 8)}`;
608
+ const result = await processDirectives(templateContent, componentContext, componentFilePath, componentOptions, dependencies), preservedExpressions = new Set(Array.isArray(parentContext.__stx_preserved_client_expressions) ? parentContext.__stx_preserved_client_expressions : []);
609
+ for (const match of result.matchAll(/\{\{([\s\S]*?)\}\}/g))
610
+ preservedExpressions.add(match[1].trim());
611
+ if (preservedExpressions.size > 0)
612
+ parentContext.__stx_preserved_client_expressions = [...preservedExpressions];
613
+ const scopeId = `stx_${baseName.replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}_${Math.random().toString(36).slice(2, 8)}`;
588
614
  let output = result;
589
615
  if (hasSignalScripts) {
590
616
  let propsJson = "";
@@ -594,15 +620,30 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
594
620
  } catch {}
595
621
  const hydrateJson = hydrateTrigger ? ` stx-hydrate="${escapeAttr(hydrateTrigger)}"` : "";
596
622
  output = `<div data-stx-scope="${scopeId}"${propsJson}${hydrateJson}>${result}</div>`;
597
- output = clientScripts.map((script) => {
623
+ const scopedScripts = await Promise.all(clientScripts.map(async (script) => {
598
624
  const scriptMatch = script.match(/<script([^>]*)>([\s\S]*)<\/script>/i);
599
625
  if (!scriptMatch)
600
626
  return script;
601
- const [, attrs, rawContent] = scriptMatch, content = transformStoreImports(rawContent), wrappedContent = `
627
+ const [, attrs, rawContent] = scriptMatch, cssExtraction = extractAndStripCssImports(rawContent, {
628
+ filePath: componentFilePath,
629
+ projectRoot: process.cwd()
630
+ });
631
+ let bundledContent = cssExtraction.code;
632
+ const vendorStyleTags = renderVendorStyleTags(cssExtraction.styles), { hasUserImports, bundleClientScript } = await import("./client-script-bundler.js");
633
+ if (hasUserImports(bundledContent))
634
+ bundledContent = await bundleClientScript(bundledContent, componentFilePath, {
635
+ projectRoot: process.cwd(),
636
+ minify: options.buildMode === "compile"
637
+ });
638
+ const { rewriteStxImportSpecifiers } = await import("./signal-processing.js");
639
+ bundledContent = bundledContent.replace(/^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"](?:stx|@stacksjs\/stx|@stacksjs\/browser)['"]\s*;?\s*$/gm, (_match, specifiers) => rewriteStxImportSpecifiers(specifiers));
640
+ const content = transformStoreImports(bundledContent), wrappedContent = `
602
641
  (function() {
603
- const { state, derived, effect, batch, defineEmits, defineExpose, defineSlots, defineProps, withDefaults, 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, inject, ref, reactive, computed, watch, watchEffect, nextTick } = window.stx;
642
+ ${buildRuntimeGlobalsDestructure("const", COMPONENT_SCOPE_LOCAL_GLOBALS)}
604
643
  const __scope = window.stx._scopes = window.stx._scopes || {};
605
644
  const __scopeVars = __scope['${scopeId}'] = __scope['${scopeId}'] || {};
645
+ const __previousCurrentElement = window.__STX_CURRENT_ELEMENT__;
646
+ window.__STX_CURRENT_ELEMENT__ = document.querySelector('[data-stx-scope="${scopeId}"]');
606
647
 
607
648
  // Scope-specific lifecycle callbacks
608
649
  __scopeVars.__mountCallbacks = __scopeVars.__mountCallbacks || [];
@@ -610,6 +651,7 @@ ${triedPaths.map((p) => ` - ${p}`).join(`
610
651
  const onMount = (fn) => __scopeVars.__mountCallbacks.push(fn);
611
652
  const onDestroy = (fn) => __scopeVars.__destroyCallbacks.push(fn);
612
653
 
654
+ try {
613
655
  ${content}
614
656
 
615
657
  // Register all defined signals and functions in this scope
@@ -620,13 +662,18 @@ ${content}
620
662
  }
621
663
  catch (e) {}
622
664
  Object.assign(__scopeVars, __localVars);
665
+ }
666
+ finally {
667
+ window.__STX_CURRENT_ELEMENT__ = __previousCurrentElement;
668
+ }
623
669
  })();`;
624
670
  if (hydrateTrigger)
625
- return `<script type="stx/island" data-stx-island="${scopeId}" data-stx-scoped${attrs}>${wrappedContent}</script>`;
626
- return `<script data-stx-scoped${attrs}>${wrappedContent}</script>`;
627
- }).join(`
628
- `) + `
629
- ` + output;
671
+ return `${vendorStyleTags}<script type="stx/island" data-stx-island="${scopeId}" data-stx-scoped${attrs}>${wrappedContent}</script>`;
672
+ return `${vendorStyleTags}<script data-stx-scoped${attrs}>${wrappedContent}</script>`;
673
+ }));
674
+ output = output + `
675
+ ` + scopedScripts.join(`
676
+ `);
630
677
  if (preservedStyle)
631
678
  output += `
632
679
  ` + preservedStyle;
@@ -328,7 +328,7 @@ export async function extractVariables(scriptContent, context, filePath, options
328
328
  ...head.title && { title: head.title },
329
329
  meta: head.meta ? [...head.meta] : []
330
330
  };
331
- }, ref = (val) => ({ value: val }), reactive = (obj) => obj, computed = (fn) => ({ value: typeof fn === "function" ? fn() : fn }), watch = (_source, _cb) => {}, onMounted = (_fn) => {}, onUnmounted = (_fn) => {}, nextTick = (fn) => fn ? fn() : Promise.resolve(), defineEmits = (_events) => (_event, ..._args) => {}, defineExpose = (_exposed) => {}, provide = (_key, _value) => {}, inject = (_key, defaultValue) => defaultValue, useColorMode = () => ({ value: "light" }), useDark = () => ({ value: !1 }), mockWindow = {
331
+ }, ref = (val) => ({ value: val }), reactive = (obj) => obj, computed = (fn) => ({ value: typeof fn === "function" ? fn() : fn }), watch = (_source, _cb) => {}, onMounted = (_fn) => {}, onUnmounted = (_fn) => {}, nextTick = (fn) => fn ? fn() : Promise.resolve(), defineEmits = (_events) => (_event, ..._args) => {}, defineExpose = (_exposed) => {}, provide = (_key, _value) => {}, inject = (_key, defaultValue) => defaultValue, useColorMode = () => ({ value: "light" }), useDark = () => state(!1), useMediaQuery = (_query) => state(!1), usePreferredDark = () => useMediaQuery("(prefers-color-scheme: dark)"), usePreferredLight = () => useMediaQuery("(prefers-color-scheme: light)"), usePreferredReducedMotion = () => useMediaQuery("(prefers-reduced-motion: reduce)"), usePreferredContrast = () => useMediaQuery("(prefers-contrast: more)"), mockWindow = {
332
332
  siteId: void 0,
333
333
  API_ENDPOINT: "",
334
334
  ANALYTICS_SITE_ID: void 0,
@@ -463,11 +463,11 @@ export async function extractVariables(scriptContent, context, filePath, options
463
463
  scriptContextKeys.push(filteredContextKeys[i]);
464
464
  scriptContextValues.push(filteredContextValues[i]);
465
465
  }
466
- const result = await Function("module", "exports", "require", "props", "$props", "defineProps", "withDefaults", "state", "derived", "effect", "batch", "onMount", "onDestroy", "definePageMeta", "useRoute", "useRouter", "useHead", "useSeoMeta", "ref", "reactive", "computed", "watch", "onMounted", "onUnmounted", "nextTick", "defineEmits", "defineExpose", "defineSlots", "provide", "inject", "useColorMode", "useDark", "window", "document", "console", "confirm", "alert", "fetch", "params", ...propArgNames, ...scriptContextKeys, `return (async () => {
466
+ const result = await Function("module", "exports", "require", "props", "$props", "defineProps", "withDefaults", "state", "derived", "effect", "batch", "onMount", "onDestroy", "definePageMeta", "useRoute", "useRouter", "useHead", "useSeoMeta", "ref", "reactive", "computed", "watch", "onMounted", "onUnmounted", "nextTick", "defineEmits", "defineExpose", "defineSlots", "provide", "inject", "useColorMode", "useDark", "useMediaQuery", "usePreferredDark", "usePreferredLight", "usePreferredReducedMotion", "usePreferredContrast", "window", "document", "console", "confirm", "alert", "fetch", "params", ...propArgNames, ...scriptContextKeys, `return (async () => {
467
467
  ${convertedScript}
468
468
  ${reSyncCode}
469
469
  return module.exports
470
- })()`)(module, exports, requireFn, propsObj, $props, defineProps, withDefaults, state, derived, effect, batch, onMount, onDestroy, definePageMeta, useRoute, useRouter, useHead, useSeoMeta, ref, reactive, computed, watch, onMounted, onUnmounted, nextTick, defineEmits, defineExpose, defineSlots, provide, inject, useColorMode, useDark, windowProxy, mockDocument, mockConsole, mockWindow.confirm, mockWindow.alert, mockWindow.fetch, paramsObj, ...propArgValues, ...scriptContextValues);
470
+ })()`)(module, exports, requireFn, propsObj, $props, defineProps, withDefaults, state, derived, effect, batch, onMount, onDestroy, definePageMeta, useRoute, useRouter, useHead, useSeoMeta, ref, reactive, computed, watch, onMounted, onUnmounted, nextTick, defineEmits, defineExpose, defineSlots, provide, inject, useColorMode, useDark, useMediaQuery, usePreferredDark, usePreferredLight, usePreferredReducedMotion, usePreferredContrast, windowProxy, mockDocument, mockConsole, mockWindow.confirm, mockWindow.alert, mockWindow.fetch, paramsObj, ...propArgValues, ...scriptContextValues);
471
471
  if (options.preserveExisting) {
472
472
  for (const [key, value] of Object.entries(result))
473
473
  if (!(key in context) || context[key] === void 0)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.113",
4
+ "version": "0.2.115",
5
5
  "description": "A performant UI Framework. Powered by Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -117,16 +117,14 @@
117
117
  "dependencies": {
118
118
  "@cwcss/crosswind": "^0.2.12",
119
119
  "@stacksjs/clapp": "^0.2.0",
120
- "@stacksjs/sanitizer": "0.2.113",
120
+ "@stacksjs/desktop": "0.2.115",
121
+ "@stacksjs/sanitizer": "0.2.115",
121
122
  "@stacksjs/ts-i18n": "^0.1.11",
122
- "bun-plugin-stx": "0.2.113",
123
+ "bun-plugin-stx": "0.2.115",
123
124
  "bunfig": "^0.15.15",
124
- "stx-router": "0.2.113",
125
+ "stx-router": "0.2.115",
125
126
  "ts-broadcasting": "^0.0.9",
126
127
  "ts-images": "^0.1.9",
127
128
  "ts-syntax-highlighter": "^0.2.1"
128
- },
129
- "optionalDependencies": {
130
- "@stacksjs/desktop": "0.2.113"
131
129
  }
132
130
  }
package/stx.d.ts CHANGED
@@ -233,6 +233,14 @@ declare function useColorMode(): {
233
233
  setMode: (mode: 'light' | 'dark' | 'auto') => void
234
234
  }
235
235
  declare function useDark(): StxSignal<boolean>
236
+ declare function useMediaQuery(query: string): StxSignal<boolean> & {
237
+ readonly matches: boolean
238
+ readonly value: boolean
239
+ }
240
+ declare function usePreferredDark(): ReturnType<typeof useMediaQuery>
241
+ declare function usePreferredLight(): ReturnType<typeof useMediaQuery>
242
+ declare function usePreferredReducedMotion(): ReturnType<typeof useMediaQuery>
243
+ declare function usePreferredContrast(): ReturnType<typeof useMediaQuery>
236
244
 
237
245
  // ============================================================================
238
246
  // Head / SEO