@stacksjs/stx 0.2.152 → 0.2.153

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.
@@ -11,6 +11,7 @@ import {
11
11
  stopHmrServer
12
12
  } from "../hot-reload.js";
13
13
  import { partialsCache } from "../includes.js";
14
+ import { BUILD_ID_HEADER, getBuildId } from "../build-id.js";
14
15
  import { stateDir } from "../state-dir.js";
15
16
  import { plugin as stxPlugin } from "../plugin.js";
16
17
  import { createRouter, matchRoute, formatRoutes, findErrorPage } from "../router.js";
@@ -74,14 +75,16 @@ function serveStaticFile(filePath) {
74
75
  export async function serveApp(appDir = ".", options = {}) {
75
76
  const port = options.port || 3000, watch = options.watch !== !1, hotReload = options.hotReload !== !1 && watch;
76
77
  let hmrServer = null, actualHmrPort = options.hmrPort || port + 1;
77
- const absoluteAppDir = path.resolve(appDir), { loadStxConfig } = await import("../index.js"), projectConfig = await loadStxConfig(absoluteAppDir), stxRoot = projectConfig.root && projectConfig.root !== "." ? path.join(absoluteAppDir, projectConfig.root) : absoluteAppDir, pagesDirName = projectConfig.pagesDir || "pages", pagesDir = path.join(stxRoot, pagesDirName);
78
+ const absoluteAppDir = path.resolve(appDir), { loadStxConfig } = await import("../index.js"), projectConfig = await loadStxConfig(absoluteAppDir), stxRoot = projectConfig.root && projectConfig.root !== "." ? path.join(absoluteAppDir, projectConfig.root) : absoluteAppDir, pagesDirName = options.pagesDir || projectConfig.pagesDir || "pages", pagesDir = path.join(stxRoot, pagesDirName);
78
79
  if (!fs.existsSync(pagesDir)) {
79
- console.error(`${colors.red}Error: No '${pagesDirName}' directory found in ${colors.bright}${stxRoot}${colors.reset}`);
80
- console.log(`${colors.dim}Create a ${pagesDirName}/ directory with .stx files to define your routes.${colors.reset}`);
81
- console.log(`${colors.dim}Example: ${pagesDirName}/index.stx for the homepage.${colors.reset}`);
80
+ console.error(`${colors.red}Error: No '${pagesDirName}' directory found${colors.reset}`);
81
+ console.log(`${colors.dim}Looked in: ${colors.bright}${pagesDir}${colors.reset}`);
82
+ console.log(`${colors.dim}Run stx dev from the app directory, pass it as an argument (stx dev <dir>),${colors.reset}`);
83
+ console.log(`${colors.dim}point at another directory with --pages <dir>, or set pagesDir in stx.config.ts.${colors.reset}`);
84
+ console.log(`${colors.dim}Starting fresh? Create ${pagesDirName}/index.stx for the homepage.${colors.reset}`);
82
85
  return !1;
83
86
  }
84
- const routes = createRouter(stxRoot);
87
+ const routes = createRouter(stxRoot, { pagesDir: pagesDirName });
85
88
  if (routes.length === 0) {
86
89
  console.error(`${colors.red}Error: No page files found in ${colors.bright}${pagesDir}${colors.reset}`);
87
90
  return !1;
@@ -447,6 +450,7 @@ export async function serveApp(appDir = ".", options = {}) {
447
450
  "X-STX-Layout": layoutMetadata.layout,
448
451
  "X-STX-Layout-Group": layoutMetadata.group,
449
452
  ...pageTitle && { "X-STX-Title": encodeURIComponent(pageTitle) },
453
+ [BUILD_ID_HEADER]: getBuildId(),
450
454
  "Cache-Control": "no-store, no-cache, must-revalidate"
451
455
  }
452
456
  });
@@ -37,8 +37,14 @@ export declare const MIME_TYPES: {
37
37
  };
38
38
  /**
39
39
  * Default dev server options
40
+ *
41
+ * `pagesDir` is excluded rather than defaulted: it has no static default. Its
42
+ * absence is meaningful — it's what lets `stx.config.ts` `pagesDir` apply.
43
+ * Baking `'pages'` in here would override a configured `views/` for anyone who
44
+ * spread these defaults (#1781). serveApp resolves it as
45
+ * option → config → `'pages'`.
40
46
  */
41
- export declare const DEFAULT_DEV_OPTIONS: Required<Omit<DevServerOptions, 'stxOptions' | 'markdown'>>;
47
+ export declare const DEFAULT_DEV_OPTIONS: Required<Omit<DevServerOptions, 'stxOptions' | 'markdown' | 'pagesDir'>>;
42
48
  /**
43
49
  * Markdown rendering options
44
50
  */
@@ -59,6 +65,7 @@ export declare interface DevServerOptions {
59
65
  watch?: boolean
60
66
  native?: boolean
61
67
  titlebarHidden?: boolean
68
+ pagesDir?: string
62
69
  stxOptions?: StxOptions
63
70
  markdown?: MarkdownOptions
64
71
  cache?: boolean
@@ -1,5 +1,42 @@
1
1
  import type { AppHeadConfig } from './types/config-types';
2
2
  import type { ColorModeBootConfig } from './color-mode-boot';
3
+ /**
4
+ * Build the attribute string for the shell's `<html>` open tag.
5
+ *
6
+ * `htmlAttrs.lang` wins over the `lang` option so `useHead({ htmlAttrs: { lang } })`
7
+ * is not emitted twice (two `lang=` attributes on one element is a parse error
8
+ * that browsers resolve by keeping the first — i.e. silently ignoring the
9
+ * more specific one).
10
+ *
11
+ * @param htmlAttrs - Attributes from config `app.head.htmlAttrs` / `useHead()`
12
+ * @param lang - The `lang` option, used unless `htmlAttrs.lang` overrides it
13
+ * @returns Leading-space-prefixed attribute string, e.g. ` lang="en" class="marketing"`
14
+ */
15
+ export declare function buildHtmlAttrs(htmlAttrs?: Record<string, string>, lang?: string): string;
16
+ /**
17
+ * Merge a page's `htmlAttrs` over the config's.
18
+ *
19
+ * Per-key override, except `class`, which unions: a global
20
+ * `app.head.htmlAttrs.class` (`h-full`, a font class, …) shouldn't silently
21
+ * vanish the moment one page adds a class of its own. Same rule as
22
+ * `applyHtmlAttrs`, so "class unions, everything else overrides" holds at every
23
+ * point htmlAttrs are combined.
24
+ */
25
+ export declare function mergeHtmlAttrs(base?: Record<string, string>, override?: Record<string, string>): Record<string, string>;
26
+ /**
27
+ * Merge `htmlAttrs` into a document that rendered its OWN `<html>` tag.
28
+ *
29
+ * `generateDocumentShell` only runs for pages without a shell, so a page or
30
+ * layout that writes `<html>` itself — the common case, and what SSG emits —
31
+ * would otherwise drop `useHead({ htmlAttrs })` entirely. Whether the attribute
32
+ * lands should not depend on who wrote the document element.
33
+ *
34
+ * `class` unions with what the template already put there (the template's own
35
+ * classes are never dropped); every other attribute is overwritten. Only the
36
+ * classes stx contributed are recorded in the marker, so an SPA navigation
37
+ * removes those and leaves the hand-written ones in place.
38
+ */
39
+ export declare function applyHtmlAttrs(html: string, htmlAttrs?: Record<string, string>): string;
3
40
  /**
4
41
  * Insert the inline x-cloak <style> before </head> if a head exists and the
5
42
  * style isn't already present. Idempotent (guards on the data-stx-cloak
@@ -106,3 +143,21 @@ export declare function ensureDocumentShell(html: string, headConfig?: AppHeadCo
106
143
  * emission.
107
144
  */
108
145
  export declare const CLOAK_STYLE: '<style data-stx-cloak>[x-cloak]{display:none !important}</style>';
146
+ /**
147
+ * Markers recording which `<html>` attributes stx itself wrote from `htmlAttrs`
148
+ * (stacksjs/stx#1798).
149
+ *
150
+ * The router needs them to reconcile the root element across an SPA layout
151
+ * change. It cannot just diff `<html>` against the incoming document: by then
152
+ * the color-mode boot script has added its own `dark` class / `data-theme`, and
153
+ * `animation.ts` has added `data-reduced-motion`, none of which exist in the
154
+ * server response — a blind diff would strip them on every navigation and
155
+ * flash the wrong theme. These say exactly what stx put there, so the router
156
+ * can remove precisely that much and leave runtime state alone.
157
+ *
158
+ * `class` is tracked separately because it is the one attribute that gets
159
+ * *merged* into at runtime, so it has to be reconciled token-by-token rather
160
+ * than replaced wholesale.
161
+ */
162
+ export declare const HTML_ATTRS_MARKER: 'data-stx-html-attrs';
163
+ export declare const HTML_CLASS_MARKER: 'data-stx-html-class';
@@ -1,5 +1,104 @@
1
1
  import { generateColorModeBootScript } from "./color-mode-boot.js";
2
- export const CLOAK_STYLE = "<style data-stx-cloak>[x-cloak]{display:none !important}</style>";
2
+ export const CLOAK_STYLE = "<style data-stx-cloak>[x-cloak]{display:none !important}</style>", HTML_ATTRS_MARKER = "data-stx-html-attrs", HTML_CLASS_MARKER = "data-stx-html-class";
3
+ const SAFE_ATTR_NAME = /^[A-Za-z_:][\w:.-]*$/;
4
+ function escapeAttr(value) {
5
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6
+ }
7
+ export function buildHtmlAttrs(htmlAttrs = {}, lang = "en") {
8
+ const rest = [], managed = [];
9
+ let className = "", resolvedLang = lang;
10
+ for (const [rawName, value] of Object.entries(htmlAttrs || {})) {
11
+ if (value == null)
12
+ continue;
13
+ const name = rawName.trim();
14
+ if (!SAFE_ATTR_NAME.test(name))
15
+ continue;
16
+ const lower = name.toLowerCase();
17
+ if (lower === "lang") {
18
+ resolvedLang = String(value);
19
+ continue;
20
+ }
21
+ if (lower === "class") {
22
+ className = String(value).trim();
23
+ continue;
24
+ }
25
+ rest.push(` ${name}="${escapeAttr(String(value))}"`);
26
+ managed.push(name);
27
+ }
28
+ let out = ` lang="${escapeAttr(resolvedLang)}"`;
29
+ if (className)
30
+ out += ` class="${escapeAttr(className)}"`;
31
+ out += rest.join("");
32
+ if (className)
33
+ out += ` ${HTML_CLASS_MARKER}="${escapeAttr(className)}"`;
34
+ if (managed.length)
35
+ out += ` ${HTML_ATTRS_MARKER}="${escapeAttr(managed.join(" "))}"`;
36
+ return out;
37
+ }
38
+ export function mergeHtmlAttrs(base = {}, override = {}) {
39
+ const merged = { ...base, ...override };
40
+ if (base.class && override.class) {
41
+ const tokens = base.class.split(/\s+/).filter(Boolean);
42
+ for (const token of override.class.split(/\s+/).filter(Boolean))
43
+ if (!tokens.includes(token))
44
+ tokens.push(token);
45
+ merged.class = tokens.join(" ");
46
+ }
47
+ return merged;
48
+ }
49
+ export function applyHtmlAttrs(html, htmlAttrs = {}) {
50
+ if (!htmlAttrs || Object.keys(htmlAttrs).length === 0)
51
+ return html;
52
+ const openTag = html.match(/<html\b([^>]*)>/i);
53
+ if (!openTag)
54
+ return html;
55
+ const existing = {}, order = [], attrRe = /([A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
56
+ let m;
57
+ while ((m = attrRe.exec(openTag[1])) !== null) {
58
+ const name = m[1];
59
+ if (!(name in existing))
60
+ order.push(name);
61
+ existing[name] = m[2] ?? m[3] ?? m[4] ?? "";
62
+ }
63
+ const addedClasses = [], managed = [];
64
+ for (const [rawName, value] of Object.entries(htmlAttrs)) {
65
+ if (value == null)
66
+ continue;
67
+ const name = rawName.trim();
68
+ if (!SAFE_ATTR_NAME.test(name))
69
+ continue;
70
+ const lower = name.toLowerCase();
71
+ if (lower === "class") {
72
+ const present = (existing.class || "").split(/\s+/).filter(Boolean);
73
+ for (const token of String(value).split(/\s+/).filter(Boolean))
74
+ if (!present.includes(token)) {
75
+ present.push(token);
76
+ addedClasses.push(token);
77
+ }
78
+ if (!("class" in existing))
79
+ order.push("class");
80
+ existing.class = present.join(" ");
81
+ continue;
82
+ }
83
+ if (!(name in existing))
84
+ order.push(name);
85
+ existing[name] = String(value);
86
+ if (lower !== "lang")
87
+ managed.push(name);
88
+ }
89
+ if (addedClasses.length) {
90
+ if (!(HTML_CLASS_MARKER in existing))
91
+ order.push(HTML_CLASS_MARKER);
92
+ existing[HTML_CLASS_MARKER] = addedClasses.join(" ");
93
+ }
94
+ if (managed.length) {
95
+ if (!(HTML_ATTRS_MARKER in existing))
96
+ order.push(HTML_ATTRS_MARKER);
97
+ existing[HTML_ATTRS_MARKER] = managed.join(" ");
98
+ }
99
+ const rebuilt = order.map((name) => ` ${name}="${escapeAttr(existing[name])}"`).join("");
100
+ return html.replace(openTag[0], () => `<html${rebuilt}>`);
101
+ }
3
102
  export function injectCloakStyle(html) {
4
103
  if (html.includes("data-stx-cloak"))
5
104
  return html;
@@ -86,6 +185,7 @@ export function generateDocumentShell(content, headConfig = {}, options = {}) {
86
185
  link = [],
87
186
  script: headScriptTags = [],
88
187
  headRaw = "",
188
+ htmlAttrs = {},
89
189
  bodyClass = "",
90
190
  bodyAttrs = {}
91
191
  } = headConfig, pageTitle = options.title || configTitle, metaTags = [...[
@@ -104,7 +204,7 @@ export function generateDocumentShell(content, headConfig = {}, options = {}) {
104
204
  return ` <script>${s.content}</script>`;
105
205
  return "";
106
206
  }).filter(Boolean).join(`
107
- `), bodyAttrStr = Object.entries(bodyAttrs).map(([k, v]) => ` ${k}="${v}"`).join(""), bodyClassStr = bodyClass ? ` class="${bodyClass}"` : "", { body: pageBody, hoisted: pageHeadTags } = hoistLeadingHeadTags(content), headParts = [
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 = [
108
208
  metaTags,
109
209
  options.colorMode ? ` ${generateColorModeBootScript(options.colorMode)}` : "",
110
210
  ` <title>${pageTitle}</title>`,
@@ -122,7 +222,7 @@ export function generateDocumentShell(content, headConfig = {}, options = {}) {
122
222
  options.bodyStack || ""
123
223
  ].filter(Boolean);
124
224
  return `<!DOCTYPE html>
125
- <html lang="${lang}">
225
+ <html${htmlAttrStr}>
126
226
  <head>
127
227
  ${headParts.join(`
128
228
  `)}
package/dist/head.js CHANGED
@@ -194,7 +194,11 @@ export function applyHead(config = currentHead) {
194
194
  }
195
195
  if (config.htmlAttrs)
196
196
  for (const [key, value] of Object.entries(config.htmlAttrs))
197
- document.documentElement.setAttribute(key, value);
197
+ if (key === "class")
198
+ for (const cls of String(value).split(/\s+/).filter(Boolean))
199
+ document.documentElement.classList.add(cls);
200
+ else
201
+ document.documentElement.setAttribute(key, value);
198
202
  if (config.bodyAttrs)
199
203
  for (const [key, value] of Object.entries(config.bodyAttrs))
200
204
  document.body.setAttribute(key, value);
package/dist/includes.js CHANGED
@@ -597,7 +597,7 @@ export async function processIncludes(template, context, filePath, options, depe
597
597
  if (isSignalScript && !isServerScript) {
598
598
  signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
599
599
  const resolvedContent = transformStoreImports(scriptContent), transformedScript = transformSignalScript(resolvedContent, signalScopeId);
600
- preservedScript += `${vendorStyleTags}<script data-stx-scoped>${transformedScript}</script>
600
+ preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always">${transformedScript}</script>
601
601
  `;
602
602
  continue;
603
603
  }
@@ -613,7 +613,7 @@ export async function processIncludes(template, context, filePath, options, depe
613
613
  const extraAttrs = scriptAttrs.replace(/\bclient\b/i, "").trim();
614
614
  signalScopeId = `stx_scope_${path.basename(includeFilePath, ".stx").replace(/[^a-zA-Z0-9]/g, "_")}_${++scopeIdCounter}`;
615
615
  const resolvedContent = transformStoreImports(scriptContent), tail = buildScopeRegistrationTail(resolvedContent, signalScopeId), wrapped = wrapClientScript(resolvedContent, tail);
616
- preservedScript += `${vendorStyleTags}<script data-stx-scoped${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
616
+ preservedScript += `${vendorStyleTags}<script data-stx-scoped data-stx-run="always"${extraAttrs ? ` ${extraAttrs}` : ""}>${wrapped}</script>
617
617
  `;
618
618
  }
619
619
  }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,22 @@ export { type HydrationOptions } from './hydration';
28
28
  // shapes with different field sets; those are kept private to their modules
29
29
  // to avoid silently shadowing the user-facing one. Surfaced by stacksjs/stx#1707.
30
30
  export { type CookieOptions } from './composables/use-cookie';
31
+ // Storage composables were reachable ONLY via the './composables' subpath, so
32
+ // `import { useLocalStorage } from '@stacksjs/stx'` failed with TS2614 while
33
+ // `state`/`derived` imported fine — and a `functions/` composable (which the
34
+ // docs recommend for anything with persistence) had to reach for the subpath,
35
+ // where the same identifier used to return a different shape. See #1797.
36
+ export {
37
+ clearStorage,
38
+ getStorageKeys,
39
+ getStorageSize,
40
+ type StorageRef,
41
+ type StorageType,
42
+ useLocalStorage,
43
+ useSessionStorage,
44
+ useStorage,
45
+ type UseStorageOptions,
46
+ } from './composables/use-storage';
31
47
  export { formatSize, getTotalSize } from './deploy/index';
32
48
  export { type ImageRenderResult, type ImageVariant, type ProcessedImage, clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from './image-optimization/index';
33
49
  export { type AnalysisResult } from './analyzer';
@@ -62,6 +78,7 @@ export * from './async-components';
62
78
  export * from './auth';
63
79
  export * from './build';
64
80
  export * from './build-assets';
81
+ export * from './build-id';
65
82
  export * from './build-mode-detector';
66
83
  export * from './build-optimizer';
67
84
  export * from './broadcasting';
package/dist/index.js CHANGED
@@ -1,3 +1,11 @@
1
+ export {
2
+ clearStorage,
3
+ getStorageKeys,
4
+ getStorageSize,
5
+ useLocalStorage,
6
+ useSessionStorage,
7
+ useStorage
8
+ } from "./composables/use-storage.js";
1
9
  export { formatSize, getTotalSize } from "./deploy/index.js";
2
10
  export { clearImageCache, getFallbackVariant, getMimeType, groupVariantsByFormat, optimizeImage, processImage } from "./image-optimization/index.js";
3
11
  export { fileExists } from "./utils.js";
@@ -21,6 +29,7 @@ export * from "./async-components.js";
21
29
  export * from "./auth.js";
22
30
  export * from "./build.js";
23
31
  export * from "./build-assets.js";
32
+ export * from "./build-id.js";
24
33
  export * from "./build-mode-detector.js";
25
34
  export * from "./build-optimizer.js";
26
35
  export * from "./broadcasting.js";
package/dist/process.js CHANGED
@@ -37,7 +37,8 @@ import { processVueTemplate } from "./vue-template.js";
37
37
  import { processDynamicComponents } from "./dynamic-components.js";
38
38
  import { dedupeScopedStyles, processScopedStyles } from "./style-scoping.js";
39
39
  import { injectColorModeBootScript } from "./color-mode-boot.js";
40
- import { ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags } from "./document-shell.js";
40
+ import { injectBuildId } from "./build-id.js";
41
+ import { applyHtmlAttrs, ensureDocumentShell, hasDocumentShell, injectCloakStyle, injectConfigHeadTags, mergeHtmlAttrs } 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";
@@ -276,14 +277,16 @@ export async function processDirectives(template, context, filePath, options, de
276
277
  script: [...baseHeadConfig.script || [], ...runtimeHead.script || []],
277
278
  headRaw: [baseHeadConfig.headRaw, pageHeadRaw].filter(Boolean).join(`
278
279
  `),
279
- ...runtimeHead.htmlAttrs && { htmlAttrs: { ...baseHeadConfig.htmlAttrs || {}, ...runtimeHead.htmlAttrs } },
280
+ ...runtimeHead.htmlAttrs && { htmlAttrs: mergeHtmlAttrs(baseHeadConfig.htmlAttrs, runtimeHead.htmlAttrs) },
280
281
  ...runtimeHead.bodyAttrs && { bodyAttrs: { ...baseHeadConfig.bodyAttrs || {}, ...runtimeHead.bodyAttrs } }
281
282
  }, alreadyShelled = hasDocumentShell(result);
282
283
  result = ensureDocumentShell(result, headConfig, {
283
284
  ...options.app?.colorMode && { colorMode: options.app.colorMode }
284
285
  });
285
- if (alreadyShelled)
286
+ if (alreadyShelled) {
286
287
  result = injectConfigHeadTags(result, baseHeadConfig);
288
+ result = applyHtmlAttrs(result, headConfig.htmlAttrs);
289
+ }
287
290
  const layoutComment = result.match(/<!-- stx-layout: ([^ ]+) -->/);
288
291
  if (layoutComment && result.includes("<head")) {
289
292
  const layoutName = layoutComment[1], layoutGroup = deriveLayoutGroup(layoutName);
@@ -303,6 +306,7 @@ export async function processDirectives(template, context, filePath, options, de
303
306
  const colorModeConfig = options.app?.colorMode;
304
307
  if (colorModeConfig)
305
308
  result = injectColorModeBootScript(result, colorModeConfig);
309
+ result = injectBuildId(result);
306
310
  result = placeSignalsRuntimeBeforeScripts(result);
307
311
  }
308
312
  if (isTopLevel)
@@ -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]) {