@stacksjs/stx 0.2.152 → 0.2.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/animation.js +6 -6
  2. package/dist/app-shell.js +2 -1
  3. package/dist/browser-composables.d.ts +8 -0
  4. package/dist/build-id.d.ts +49 -0
  5. package/dist/build-id.js +20 -0
  6. package/dist/build-message.d.ts +10 -0
  7. package/dist/build-message.js +26 -0
  8. package/dist/builtins/attrs.js +6 -1
  9. package/dist/cli.js +1743 -1255
  10. package/dist/client-script-bundler.js +2 -4
  11. package/dist/client-script.d.ts +29 -0
  12. package/dist/client-script.js +142 -97
  13. package/dist/color-mode-boot.d.ts +46 -10
  14. package/dist/color-mode-boot.js +53 -13
  15. package/dist/component-client-factories.js +3 -2
  16. package/dist/component-renderer.js +10 -21
  17. package/dist/composables/index.d.ts +8 -1
  18. package/dist/composables/use-clipboard.d.ts +1 -5
  19. package/dist/composables/use-clipboard.js +57 -46
  20. package/dist/composables/use-color-mode.d.ts +19 -4
  21. package/dist/composables/use-color-mode.js +49 -28
  22. package/dist/composables/use-router.d.ts +28 -11
  23. package/dist/composables/use-router.js +63 -9
  24. package/dist/composables/use-storage.d.ts +14 -15
  25. package/dist/composables/use-storage.js +63 -2
  26. package/dist/config.js +22 -7
  27. package/dist/craft.js +1319 -841
  28. package/dist/dev-server/crosswind.js +8 -6
  29. package/dist/dev-server/serve-app.js +13 -7
  30. package/dist/dev-server/serve-markdown.js +4 -1
  31. package/dist/dev-server/serve-multi.js +32 -20
  32. package/dist/dev-server/types.d.ts +8 -1
  33. package/dist/document-shell.d.ts +120 -0
  34. package/dist/document-shell.js +161 -14
  35. package/dist/dynamic-components.js +123 -4
  36. package/dist/events.d.ts +26 -49
  37. package/dist/events.js +23 -9
  38. package/dist/expressions.d.ts +1 -1
  39. package/dist/expressions.js +3 -3
  40. package/dist/framework-composables.d.ts +41 -0
  41. package/dist/framework-composables.js +115 -0
  42. package/dist/head.js +5 -1
  43. package/dist/hot-reload.js +8 -2
  44. package/dist/includes.js +4 -4
  45. package/dist/index.d.ts +17 -0
  46. package/dist/index.js +9 -0
  47. package/dist/isolated-build.d.ts +12 -0
  48. package/dist/isolated-build.js +18 -0
  49. package/dist/plugin-system.d.ts +1 -1
  50. package/dist/process.js +46 -12
  51. package/dist/production-builder.js +3 -1
  52. package/dist/production-server.js +4 -1
  53. package/dist/prop-sentinels.d.ts +31 -0
  54. package/dist/prop-sentinels.js +7 -0
  55. package/dist/pwa.js +1258 -780
  56. package/dist/reactive.js +1 -1
  57. package/dist/reactivity.d.ts +0 -15
  58. package/dist/reactivity.js +15 -6
  59. package/dist/runtime-globals.d.ts +40 -2
  60. package/dist/runtime-globals.js +90 -0
  61. package/dist/runtime-injection.d.ts +51 -0
  62. package/dist/runtime-injection.js +13 -0
  63. package/dist/script-emit.d.ts +20 -0
  64. package/dist/script-emit.js +11 -0
  65. package/dist/script-validation.js +12 -7
  66. package/dist/signal-processing.js +25 -40
  67. package/dist/signals-api.d.ts +30 -0
  68. package/dist/signals-api.js +18 -0
  69. package/dist/signals.js +532 -85
  70. package/dist/site-builder/build.js +21 -7
  71. package/dist/site-builder/seo.js +19 -10
  72. package/dist/site-builder/theme.js +5 -4
  73. package/dist/site-builder/types.d.ts +1 -0
  74. package/dist/ssg.js +1255 -777
  75. package/dist/state-management.js +4 -1
  76. package/dist/store-loader.d.ts +0 -11
  77. package/dist/store-loader.js +27 -3
  78. package/dist/strict-lint.d.ts +9 -6
  79. package/dist/strict-lint.js +2 -21
  80. package/dist/stx.d.ts +125 -22
  81. package/dist/types/config-types.d.ts +1 -0
  82. package/dist/unresolved-identifiers.d.ts +50 -0
  83. package/dist/unresolved-identifiers.js +146 -0
  84. package/dist/utils.js +3 -3
  85. package/dist/variable-extractor.js +17 -1
  86. package/dist/x-element.js +1 -1
  87. package/package.json +5 -5
  88. package/stx.d.ts +125 -22
@@ -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,8 +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";
40
- import { ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags } from "./document-shell.js";
39
+ import { injectColorModeBootScript, normalizeCriticalHeadOrder } from "./color-mode-boot.js";
40
+ import { injectBuildId } from "./build-id.js";
41
+ import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs, startsDocument } from "./document-shell.js";
41
42
  import { hasSignalsSyntax, convertSignalDirectivesToAttributes, convertSignalLoopsToAttributes, preEvalLiteralReactiveIfs, processSignals } from "./signal-processing.js";
42
43
  import { processComponents } from "./component-renderer.js";
43
44
  import { processInlineAssets } from "./inline-assets.js";
@@ -50,6 +51,7 @@ import { deriveLayoutGroup } from "stx-router/layout-metadata";
50
51
  import { initializeComponentClientFactories, injectComponentClientFactories } from "./component-client-factories.js";
51
52
  import { importOnce } from "./lazy-module.js";
52
53
  export { injectRouterScript } from "./runtime-injection.js";
54
+ import { injectSignalsRuntime, outputNeedsSignalsRuntime, pageShipsSignalsRuntime } from "./runtime-injection.js";
53
55
  export { processJsonDirective, processOnceDirective } from "./misc-directives.js";
54
56
  export { validateClientScript } from "./script-validation.js";
55
57
  const PARENT_PLACEHOLDER = "<!--PARENT_CONTENT_PLACEHOLDER-->";
@@ -245,6 +247,8 @@ function placeSignalsRuntimeBeforeScripts(html) {
245
247
  export async function processDirectives(template, context, filePath, options, dependencies) {
246
248
  if (options.ssr === !1 && !options.autoShell && options.buildMode === "spa")
247
249
  return template;
250
+ if (context.__stx_signals_gate === void 0)
251
+ context.__stx_signals_gate = usesSignalsInScript(template);
248
252
  const isTopLevel = !context.__stxProcessingDepth;
249
253
  if (isTopLevel)
250
254
  initializeComponentClientFactories(context);
@@ -268,7 +272,7 @@ export async function processDirectives(template, context, filePath, options, de
268
272
  pageHeadTitle = rawTitleMatch[1].trim();
269
273
  pageHeadRaw = pageHeadRaw.replace(rawTitleMatch[0], "").trim();
270
274
  }
271
- 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 = {
272
276
  ...baseHeadConfig,
273
277
  ...pageTitle && { title: pageTitle },
274
278
  meta: [...baseHeadConfig.meta || [], ...runtimeHead.meta || []],
@@ -276,18 +280,21 @@ export async function processDirectives(template, context, filePath, options, de
276
280
  script: [...baseHeadConfig.script || [], ...runtimeHead.script || []],
277
281
  headRaw: [baseHeadConfig.headRaw, pageHeadRaw].filter(Boolean).join(`
278
282
  `),
279
- ...runtimeHead.htmlAttrs && { htmlAttrs: { ...baseHeadConfig.htmlAttrs || {}, ...runtimeHead.htmlAttrs } },
283
+ ...runtimeHead.htmlAttrs && { htmlAttrs: mergeHtmlAttrs(baseHeadConfig.htmlAttrs, runtimeHead.htmlAttrs) },
280
284
  ...runtimeHead.bodyAttrs && { bodyAttrs: { ...baseHeadConfig.bodyAttrs || {}, ...runtimeHead.bodyAttrs } }
281
285
  }, alreadyShelled = hasDocumentShell(result);
282
286
  result = ensureDocumentShell(result, headConfig, {
287
+ ...titleIsHtml && { titleIsHtml: !0 },
283
288
  ...options.app?.colorMode && { colorMode: options.app.colorMode }
284
289
  });
285
- if (alreadyShelled)
290
+ if (alreadyShelled) {
286
291
  result = injectConfigHeadTags(result, baseHeadConfig);
292
+ result = applyHtmlAttrs(result, headConfig.htmlAttrs);
293
+ }
287
294
  const layoutComment = result.match(/<!-- stx-layout: ([^ ]+) -->/);
288
295
  if (layoutComment && result.includes("<head")) {
289
296
  const layoutName = layoutComment[1], layoutGroup = deriveLayoutGroup(layoutName);
290
- result = result.replace("<head>", `<head>
297
+ result = result.replace(/<head\b[^>]*>/i, (match) => `${match}
291
298
  <meta name="stx-layout" content="${escapeHtmlAttribute(layoutName)}">
292
299
  <meta name="stx-layout-group" content="${escapeHtmlAttribute(layoutGroup)}">`);
293
300
  }
@@ -303,6 +310,7 @@ export async function processDirectives(template, context, filePath, options, de
303
310
  const colorModeConfig = options.app?.colorMode;
304
311
  if (colorModeConfig)
305
312
  result = injectColorModeBootScript(result, colorModeConfig);
313
+ result = injectBuildId(result);
306
314
  result = placeSignalsRuntimeBeforeScripts(result);
307
315
  }
308
316
  if (isTopLevel)
@@ -315,6 +323,16 @@ export async function processDirectives(template, context, filePath, options, de
315
323
  ` + storeTag + result.slice(runtime.end);
316
324
  }
317
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 {}
318
336
  if (isTopLevel)
319
337
  try {
320
338
  const { getComposableScript } = await importOnce("stx/composable-loader", () => import("./composable-loader.js")), resolvedComposablesDir = options.composablesDir, composableCode = await getComposableScript(resolvedComposablesDir);
@@ -325,6 +343,8 @@ export async function processDirectives(template, context, filePath, options, de
325
343
  ` + composableTag + result.slice(anchor.end);
326
344
  }
327
345
  } catch {}
346
+ if (isTopLevel)
347
+ result = normalizeCriticalHeadOrder(result);
328
348
  if (isTopLevel && context.__stx_inject_css !== !1)
329
349
  result = await injectCrosswindCSS(result, void 0, options.buildMode === "serve");
330
350
  if (isTopLevel)
@@ -380,8 +400,8 @@ async function processDirectivesInternal(template, context, filePath, options, d
380
400
  output = output.replace(/@(?:layout|extends)\(\s*['"]([^'"]+)['"]\s*\)/g, "");
381
401
  }
382
402
  if (!layoutPath && !hasNoLayout && opts.defaultLayout) {
383
- const hasDoctype = /<!DOCTYPE\s/i.test(output), hasSections = /@section\s*\(/.test(output);
384
- if (!hasDoctype || hasSections) {
403
+ const isDocument = startsDocument(output), hasSections = /@section\s*\(/.test(output);
404
+ if (!isDocument || hasSections) {
385
405
  let autoLayoutPath = "", searchDir = path.dirname(filePath);
386
406
  const rootDir = opts.layoutsDir ? path.dirname(opts.layoutsDir) : process.cwd();
387
407
  for (let i = 0;i < 20; i++) {
@@ -598,8 +618,8 @@ async function processOtherDirectives(template, context, filePath, options, depe
598
618
  const { processStreamDirectives } = await importOnce("stx/streaming", () => import("./streaming.js"));
599
619
  output = processStreamDirectives(output, context);
600
620
  output = processLoops(output, context, filePath, opts);
601
- output = await processComponents(output, context, filePath, opts, dependencies);
602
621
  output = await processDynamicComponents(output, context, filePath, opts, dependencies);
622
+ output = await processComponents(output, context, filePath, opts, dependencies);
603
623
  output = processAnimationDirectives(output, context, filePath, opts);
604
624
  const { processDeferDirectives } = await importOnce("stx/defer", () => import("./defer.js"));
605
625
  output = processDeferDirectives(output, context, filePath);
@@ -735,7 +755,7 @@ ${snippet}
735
755
  }
736
756
  const hasClientScripts = /<script\b(?![^>]*\bserver\b)[^>]*>[\s\S]*?<\/script>/i.test(output), hasStxUsage = /\bSTX\.\w+/.test(output), alreadyHasRuntime = /window\.STX\s*=/.test(output);
737
757
  if (hasClientScripts && hasStxUsage && !alreadyHasRuntime) {
738
- const runtimeScript = `<script data-stx-scoped>
758
+ const runtimeScript = `<script data-stx-scoped data-stx-run="once">
739
759
  ${generateLifecycleRuntime()}
740
760
  </script>`;
741
761
  if (output.includes("</head>")) {
@@ -785,12 +805,26 @@ ${runtimeScript}`);
785
805
  for (const { match, attrs, content } of clientScriptMatches) {
786
806
  clientScriptsTransformed = !0;
787
807
  let processedContent = content;
788
- validateClientScript(content, filePath, options.strict);
808
+ validateClientScript(content, filePath, options.strict, context.__originalFilePath);
789
809
  const scriptResult = await processClientScript(content, { eventBindings, attrs, hasColonDirectives, templateContent: template, filePath, projectRoot: process.cwd(), production: options.buildMode === "compile", serverData });
790
810
  output = output.replace(match, () => scriptResult);
791
811
  }
792
812
  if (clientScriptsTransformed)
793
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);
794
828
  if (opts.csp?.enabled && opts.csp.useNonce && typeof context.cspNonce === "string")
795
829
  output = addNonceToInlineContent(output, context.cspNonce);
796
830
  return output;
@@ -5,7 +5,7 @@ import { buildRuntimeAsset, buildRouterAsset } from "./build-assets.js";
5
5
  import { compileTemplate } from "./template-compiler.js";
6
6
  import { generateManifest, writeManifest } from "./manifest.js";
7
7
  import { injectColorModeBootScript } from "./color-mode-boot.js";
8
- import { ensureDocumentShell } from "./document-shell.js";
8
+ import { applyHtmlAttrs, ensureDocumentShell } from "./document-shell.js";
9
9
  import { extractContainerContent } from "./app-shell.js";
10
10
  import { injectCrosswindCSS } from "./dev-server/crosswind.js";
11
11
  import { loadStxConfig } from "./config.js";
@@ -88,6 +88,8 @@ export async function buildForProduction(options = {}) {
88
88
  });
89
89
  if (colorModeConfig)
90
90
  compiled.html = injectColorModeBootScript(compiled.html, colorModeConfig);
91
+ if (headConfigDefault.htmlAttrs)
92
+ compiled.html = applyHtmlAttrs(compiled.html, headConfigDefault.htmlAttrs);
91
93
  if (!/<body[^>]*\bdata-stx=/.test(compiled.html)) {
92
94
  const dataStxMatch = compiled.html.match(/<(?!body\b)[a-zA-Z][a-zA-Z0-9-]*\b[^>]*?\s(data-stx="[^"]+")[^>]*>/);
93
95
  if (dataStxMatch && dataStxMatch[1]) {
@@ -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
+ }