nukejs 0.0.6 → 0.0.8

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 (49) hide show
  1. package/README.md +89 -6
  2. package/dist/{as-is/Link.js → Link.js} +3 -1
  3. package/dist/Link.js.map +7 -0
  4. package/dist/app.d.ts +3 -2
  5. package/dist/app.js +3 -13
  6. package/dist/app.js.map +2 -2
  7. package/dist/build-common.d.ts +6 -0
  8. package/dist/build-common.js +20 -6
  9. package/dist/build-common.js.map +2 -2
  10. package/dist/build-node.d.ts +1 -1
  11. package/dist/build-node.js +6 -17
  12. package/dist/build-node.js.map +2 -2
  13. package/dist/build-vercel.js +1 -1
  14. package/dist/build-vercel.js.map +2 -2
  15. package/dist/builder.d.ts +4 -10
  16. package/dist/builder.js +7 -38
  17. package/dist/builder.js.map +2 -2
  18. package/dist/bundle.js +60 -4
  19. package/dist/bundle.js.map +2 -2
  20. package/dist/component-analyzer.d.ts +6 -0
  21. package/dist/component-analyzer.js +12 -1
  22. package/dist/component-analyzer.js.map +2 -2
  23. package/dist/hmr-bundle.js +17 -4
  24. package/dist/hmr-bundle.js.map +2 -2
  25. package/dist/html-store.d.ts +7 -0
  26. package/dist/html-store.js.map +2 -2
  27. package/dist/http-server.d.ts +2 -9
  28. package/dist/http-server.js +16 -2
  29. package/dist/http-server.js.map +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/renderer.js +2 -7
  34. package/dist/renderer.js.map +2 -2
  35. package/dist/router.d.ts +20 -19
  36. package/dist/router.js +14 -6
  37. package/dist/router.js.map +2 -2
  38. package/dist/ssr.js +21 -4
  39. package/dist/ssr.js.map +2 -2
  40. package/dist/use-html.js +5 -1
  41. package/dist/use-html.js.map +2 -2
  42. package/dist/{as-is/useRouter.js → use-router.js} +1 -1
  43. package/dist/{as-is/useRouter.js.map → use-router.js.map} +2 -2
  44. package/package.json +1 -1
  45. package/dist/as-is/Link.js.map +0 -7
  46. package/dist/as-is/Link.tsx +0 -20
  47. package/dist/as-is/useRouter.ts +0 -33
  48. /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
  49. /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/dist/ssr.js CHANGED
@@ -74,15 +74,21 @@ function renderStyleTag(tag) {
74
74
  return ` <style${media}>${tag.content ?? ""}</style>`;
75
75
  }
76
76
  function renderManagedHeadTags(store) {
77
+ const headScripts = store.script.filter((s) => (s.position ?? "head") === "head");
77
78
  const tags = [
78
79
  ...store.meta.map(renderMetaTag),
79
80
  ...store.link.map(renderLinkTag),
80
81
  ...store.style.map(renderStyleTag),
81
- ...store.script.map(renderScriptTag)
82
+ ...headScripts.map(renderScriptTag)
82
83
  ];
83
84
  if (tags.length === 0) return [];
84
85
  return [" <!--n-head-->", ...tags, " <!--/n-head-->"];
85
86
  }
87
+ function renderManagedBodyScripts(store) {
88
+ const bodyScripts = store.script.filter((s) => s.position === "body");
89
+ if (bodyScripts.length === 0) return [];
90
+ return [" <!--n-body-scripts-->", ...bodyScripts.map(renderScriptTag), " <!--/n-body-scripts-->"];
91
+ }
86
92
  async function serverSideRender(url, res, pagesDir, isDev = false) {
87
93
  const skipClientSSR = url.includes("__hmr=1");
88
94
  const cleanUrl = url.split("?")[0];
@@ -95,13 +101,22 @@ async function serverSideRender(url, res, pagesDir, isDev = false) {
95
101
  }
96
102
  const { filePath, params, routePattern } = routeMatch;
97
103
  log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);
104
+ const searchParams = new URL(url, "http://localhost").searchParams;
105
+ const queryParams = {};
106
+ searchParams.forEach((_, k) => {
107
+ if (!(k in params)) {
108
+ const all = searchParams.getAll(k);
109
+ queryParams[k] = all.length > 1 ? all : all[0];
110
+ }
111
+ });
112
+ const mergedParams = { ...queryParams, ...params };
98
113
  const layoutPaths = findLayoutsForRoute(filePath, pagesDir);
99
114
  const { default: PageComponent } = await tsImport(
100
115
  pathToFileURL(filePath).href,
101
116
  { parentURL: import.meta.url }
102
117
  );
103
118
  const wrappedElement = await wrapWithLayouts(
104
- createElement(PageComponent, params),
119
+ createElement(PageComponent, mergedParams),
105
120
  layoutPaths
106
121
  );
107
122
  const registry = /* @__PURE__ */ new Map();
@@ -119,7 +134,7 @@ async function serverSideRender(url, res, pagesDir, isDev = false) {
119
134
  const store = await runWithHtmlStore(async () => {
120
135
  appHtml = await renderElementToHtml(wrappedElement, ctx);
121
136
  });
122
- const pageTitle = resolveTitle(store.titleOps, "Nuke");
137
+ const pageTitle = resolveTitle(store.titleOps, "NukeJS");
123
138
  const headLines = [
124
139
  ' <meta charset="utf-8" />',
125
140
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
@@ -133,6 +148,8 @@ async function serverSideRender(url, res, pagesDir, isDev = false) {
133
148
  params,
134
149
  debug: toClientDebugLevel(getDebugLevel())
135
150
  }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
151
+ const bodyScriptLines = renderManagedBodyScripts(store);
152
+ const bodyScriptsHtml = bodyScriptLines.length > 0 ? "\n" + bodyScriptLines.join("\n") + "\n" : "";
136
153
  const html = `<!DOCTYPE html>
137
154
  ${openTag("html", store.htmlAttrs)}
138
155
  <head>
@@ -162,7 +179,7 @@ ${openTag("body", store.bodyAttrs)}
162
179
  </script>
163
180
 
164
181
  ${isDev ? '<script type="module" src="/__hmr.js"></script>' : ""}
165
- </body>
182
+ ${bodyScriptsHtml}</body>
166
183
  </html>`;
167
184
  res.setHeader("Content-Type", "text/html");
168
185
  res.end(html);
package/dist/ssr.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/ssr.ts"],
4
- "sourcesContent": ["/**\r\n * ssr.ts \u2014 Server-Side Rendering Pipeline (Dev Mode)\r\n *\r\n * Handles the full page render cycle for `nuke dev`:\r\n *\r\n * 1. Match the incoming URL to a page file in app/pages using the file-system\r\n * router.\r\n * 2. Discover the layout chain (layout.tsx files from root to the page dir).\r\n * 3. Dynamically import the page and layout modules (always fresh via tsImport).\r\n * 4. Walk the import tree to discover all \"use client\" components.\r\n * 5. Render the wrapped element tree with the async renderer.\r\n * 6. Flush the html-store (title, meta, link, script, style tags).\r\n * 7. Assemble and send the full HTML document including:\r\n * - The rendered app HTML\r\n * - An importmap pointing react/nukejs to the bundled versions\r\n * - A __n_data JSON blob with hydration IDs and runtime config\r\n * - An inline bootstrap <script> that calls initRuntime()\r\n * - (dev only) A /__hmr.js script for Hot Module Replacement\r\n *\r\n * In production, a pre-built standalone handler (generated by build-common.ts)\r\n * handles each page without dynamic imports or file-system access.\r\n *\r\n * HMR fast path:\r\n * When the request URL contains `__hmr=1` (added by the HMR client during\r\n * soft navigation), the renderer skips client-component renderToString\r\n * (ctx.skipClientSSR = true). This speeds up HMR reloads because the\r\n * client already has the DOM in place and only needs fresh server markup.\r\n *\r\n * Head tag sentinels:\r\n * Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is\r\n * wrapped in <!--n-head-->\u2026<!--/n-head--> comment sentinels. The client\r\n * runtime uses these to diff and sync head tags on SPA navigation without\r\n * touching permanent tags (charset, viewport, importmap, runtime script).\r\n * Pages with no useHtml head tags emit no sentinels.\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement } from 'react';\r\nimport { pathToFileURL } from 'url';\r\nimport { tsImport } from 'tsx/esm/api';\r\nimport type { ServerResponse } from 'http';\r\nimport { log, getDebugLevel, type DebugLevel } from './logger';\r\nimport { matchRoute, findLayoutsForRoute } from './router';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\nimport { renderElementToHtml, type RenderContext } from './renderer';\r\nimport {\r\n runWithHtmlStore,\r\n resolveTitle,\r\n type HtmlStore,\r\n type HtmlAttrs,\r\n type BodyAttrs,\r\n type MetaTag,\r\n type LinkTag,\r\n type ScriptTag,\r\n type StyleTag,\r\n} from './html-store';\r\n\r\n// \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Wraps a page element in its layout chain, outermost-first.\r\n * Each layout receives `{ children: innerElement }` as props.\r\n *\r\n * tsImport creates a fresh isolated module namespace per call so edits to\r\n * layouts are reflected immediately without a server restart.\r\n */\r\nasync function wrapWithLayouts(pageElement: any, layoutPaths: string[]): Promise<any> {\r\n let element = pageElement;\r\n // Iterate in reverse so the outermost layout wraps last (becomes the root).\r\n for (let i = layoutPaths.length - 1; i >= 0; i--) {\r\n const { default: LayoutComponent } = await tsImport(\r\n pathToFileURL(layoutPaths[i]).href,\r\n { parentURL: import.meta.url },\r\n );\r\n element = createElement(LayoutComponent, { children: element });\r\n }\r\n return element;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level conversion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts the server-side DebugLevel to the string format expected by the\r\n * browser's makeLogger() function in bundle.ts.\r\n */\r\nfunction toClientDebugLevel(level: DebugLevel): string {\r\n if (level === true) return 'verbose';\r\n if (level === 'info') return 'info';\r\n if (level === 'error') return 'error';\r\n return 'silent';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML serialization helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction escapeAttr(str: string): string {\r\n return str.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\r\n}\r\n\r\n/** Serializes an attribute map to `key=\"value\" \u2026`, omitting undefined/false values. */\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : `${k}=\"${escapeAttr(String(v))}\"`)\r\n .join(' ');\r\n}\r\n\r\n/** Returns `<tag attrs>` or `<tag>` depending on whether attrs are non-empty. */\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const str = renderAttrs(attrs);\r\n return str ? `<${tag} ${str}>` : `<${tag}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag renderers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\n\r\nfunction renderMetaTag(tag: MetaTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return ` <meta ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderLinkTag(tag: LinkTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return ` <link ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderScriptTag(tag: ScriptTag): string {\r\n const attrs: Record<string, string | boolean | undefined> = {\r\n src: tag.src,\r\n type: tag.type,\r\n crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity,\r\n defer: tag.defer,\r\n async: tag.async,\r\n nomodule: tag.noModule,\r\n };\r\n const attrStr = renderAttrs(attrs);\r\n const open = attrStr ? `<script ${attrStr}>` : '<script>';\r\n return ` ${open}${tag.src ? '' : (tag.content ?? '')}</script>`;\r\n}\r\n\r\nfunction renderStyleTag(tag: StyleTag): string {\r\n const media = tag.media ? ` media=\"${escapeAttr(tag.media)}\"` : '';\r\n return ` <style${media}>${tag.content ?? ''}</style>`;\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced head tags wrapped in <!--n-head--> sentinels.\r\n * Returns an empty array when none of the stores have any tags (no sentinels\r\n * emitted for pages that don't call useHtml).\r\n */\r\nfunction renderManagedHeadTags(store: HtmlStore): string[] {\r\n const tags = [\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...store.script.map(renderScriptTag),\r\n ];\r\n if (tags.length === 0) return [];\r\n return [' <!--n-head-->', ...tags, ' <!--/n-head-->'];\r\n}\r\n\r\n// \u2500\u2500\u2500 Main SSR handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a page for the given URL and writes the full HTML response.\r\n *\r\n * @param url The raw request URL (may include query string).\r\n * @param res Node ServerResponse to write to.\r\n * @param pagesDir Absolute path to the app/pages directory.\r\n * @param isDev When true, injects the HMR client script into the page.\r\n */\r\nexport async function serverSideRender(\r\n url: string,\r\n res: ServerResponse,\r\n pagesDir: string,\r\n isDev = false,\r\n): Promise<void> {\r\n const skipClientSSR = url.includes('__hmr=1');\r\n const cleanUrl = url.split('?')[0];\r\n\r\n // \u2500\u2500 Route resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const routeMatch = matchRoute(cleanUrl, pagesDir);\r\n if (!routeMatch) {\r\n log.verbose(`No route found for: ${url}`);\r\n res.statusCode = 404;\r\n res.end('Page not found');\r\n return;\r\n }\r\n\r\n const { filePath, params, routePattern } = routeMatch;\r\n log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // \u2500\u2500 Module import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // tsImport bypasses Node's ESM module cache entirely so edits are reflected\r\n // immediately on every request in dev.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n const { default: PageComponent } = await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n );\r\n const wrappedElement = await wrapWithLayouts(\r\n createElement(PageComponent, params),\r\n layoutPaths,\r\n );\r\n\r\n // \u2500\u2500 Client component discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Walk the import tree for both the page and its layout chain.\r\n const registry = new Map<string, string>();\r\n for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n\r\n log.verbose(\r\n `Page ${routePattern}: found ${registry.size} client component(s)`,\r\n `[${[...registry.keys()].join(', ')}]`,\r\n );\r\n\r\n // \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const ctx: RenderContext = { registry, hydrated: new Set(), skipClientSSR };\r\n\r\n let appHtml = '';\r\n const store: HtmlStore = await runWithHtmlStore(async () => {\r\n appHtml = await renderElementToHtml(wrappedElement, ctx);\r\n });\r\n\r\n // \u2500\u2500 Head assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const pageTitle = resolveTitle(store.titleOps, 'Nuke');\r\n\r\n const headLines: string[] = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n ` <title>${escapeAttr(pageTitle)}</title>`,\r\n ...renderManagedHeadTags(store),\r\n ];\r\n\r\n // \u2500\u2500 Runtime data blob \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Escape </script> sequences so the JSON cannot break out of the script tag.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated],\r\n allIds: [...registry.keys()],\r\n url,\r\n params,\r\n debug: toClientDebugLevel(getDebugLevel()),\r\n })\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n\r\n // \u2500\u2500 Full document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const html = `<!DOCTYPE html>\r\n${openTag('html', store.htmlAttrs)}\r\n<head>\r\n${headLines.join('\\n')}\r\n</head>\r\n${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n{\r\n \"imports\": {\r\n \"react\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n}\r\n </script>\r\n\r\n <script type=\"module\">\r\n await import('react');\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n initRuntime(data);\r\n </script>\r\n\r\n ${isDev ? '<script type=\"module\" src=\"/__hmr.js\"></script>' : ''}\r\n</body>\r\n</html>`;\r\n\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end(html);\r\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * ssr.ts \u2014 Server-Side Rendering Pipeline (Dev Mode)\r\n *\r\n * Handles the full page render cycle for `nuke dev`:\r\n *\r\n * 1. Match the incoming URL to a page file in app/pages using the file-system\r\n * router.\r\n * 2. Discover the layout chain (layout.tsx files from root to the page dir).\r\n * 3. Dynamically import the page and layout modules (always fresh via tsImport).\r\n * 4. Walk the import tree to discover all \"use client\" components.\r\n * 5. Render the wrapped element tree with the async renderer.\r\n * 6. Flush the html-store (title, meta, link, script, style tags).\r\n * 7. Assemble and send the full HTML document including:\r\n * - The rendered app HTML\r\n * - An importmap pointing react/nukejs to the bundled versions\r\n * - A __n_data JSON blob with hydration IDs and runtime config\r\n * - An inline bootstrap <script> that calls initRuntime()\r\n * - (dev only) A /__hmr.js script for Hot Module Replacement\r\n *\r\n * In production, a pre-built standalone handler (generated by build-common.ts)\r\n * handles each page without dynamic imports or file-system access.\r\n *\r\n * HMR fast path:\r\n * When the request URL contains `__hmr=1` (added by the HMR client during\r\n * soft navigation), the renderer skips client-component renderToString\r\n * (ctx.skipClientSSR = true). This speeds up HMR reloads because the\r\n * client already has the DOM in place and only needs fresh server markup.\r\n *\r\n * Head tag sentinels:\r\n * Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is\r\n * wrapped in <!--n-head-->\u2026<!--/n-head--> comment sentinels. The client\r\n * runtime uses these to diff and sync head tags on SPA navigation without\r\n * touching permanent tags (charset, viewport, importmap, runtime script).\r\n * Pages with no useHtml head tags emit no sentinels.\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement } from 'react';\r\nimport { pathToFileURL } from 'url';\r\nimport { tsImport } from 'tsx/esm/api';\r\nimport type { ServerResponse } from 'http';\r\nimport { log, getDebugLevel, type DebugLevel } from './logger';\r\nimport { matchRoute, findLayoutsForRoute } from './router';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\nimport { renderElementToHtml, type RenderContext } from './renderer';\r\nimport {\r\n runWithHtmlStore,\r\n resolveTitle,\r\n type HtmlStore,\r\n type HtmlAttrs,\r\n type BodyAttrs,\r\n type MetaTag,\r\n type LinkTag,\r\n type ScriptTag,\r\n type StyleTag,\r\n} from './html-store';\r\n\r\n// \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Wraps a page element in its layout chain, outermost-first.\r\n * Each layout receives `{ children: innerElement }` as props.\r\n *\r\n * tsImport creates a fresh isolated module namespace per call so edits to\r\n * layouts are reflected immediately without a server restart.\r\n */\r\nasync function wrapWithLayouts(pageElement: any, layoutPaths: string[]): Promise<any> {\r\n let element = pageElement;\r\n // Iterate in reverse so the outermost layout wraps last (becomes the root).\r\n for (let i = layoutPaths.length - 1; i >= 0; i--) {\r\n const { default: LayoutComponent } = await tsImport(\r\n pathToFileURL(layoutPaths[i]).href,\r\n { parentURL: import.meta.url },\r\n );\r\n element = createElement(LayoutComponent, { children: element });\r\n }\r\n return element;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level conversion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts the server-side DebugLevel to the string format expected by the\r\n * browser's makeLogger() function in bundle.ts.\r\n */\r\nfunction toClientDebugLevel(level: DebugLevel): string {\r\n if (level === true) return 'verbose';\r\n if (level === 'info') return 'info';\r\n if (level === 'error') return 'error';\r\n return 'silent';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML serialization helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction escapeAttr(str: string): string {\r\n return str.replace(/&/g, '&amp;').replace(/\"/g, '&quot;');\r\n}\r\n\r\n/** Serializes an attribute map to `key=\"value\" \u2026`, omitting undefined/false values. */\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : `${k}=\"${escapeAttr(String(v))}\"`)\r\n .join(' ');\r\n}\r\n\r\n/** Returns `<tag attrs>` or `<tag>` depending on whether attrs are non-empty. */\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const str = renderAttrs(attrs);\r\n return str ? `<${tag} ${str}>` : `<${tag}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag renderers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\n\r\nfunction renderMetaTag(tag: MetaTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return ` <meta ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderLinkTag(tag: LinkTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return ` <link ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderScriptTag(tag: ScriptTag): string {\r\n const attrs: Record<string, string | boolean | undefined> = {\r\n src: tag.src,\r\n type: tag.type,\r\n crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity,\r\n defer: tag.defer,\r\n async: tag.async,\r\n nomodule: tag.noModule,\r\n };\r\n const attrStr = renderAttrs(attrs);\r\n const open = attrStr ? `<script ${attrStr}>` : '<script>';\r\n return ` ${open}${tag.src ? '' : (tag.content ?? '')}</script>`;\r\n}\r\n\r\nfunction renderStyleTag(tag: StyleTag): string {\r\n const media = tag.media ? ` media=\"${escapeAttr(tag.media)}\"` : '';\r\n return ` <style${media}>${tag.content ?? ''}</style>`;\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced head tags wrapped in <!--n-head--> sentinels.\r\n * Scripts with position='body' are excluded here \u2014 they go in renderManagedBodyScripts().\r\n * Returns an empty array when none of the stores have any tags (no sentinels\r\n * emitted for pages that don't call useHtml).\r\n */\r\nfunction renderManagedHeadTags(store: HtmlStore): string[] {\r\n const headScripts = store.script.filter(s => (s.position ?? 'head') === 'head');\r\n const tags = [\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...headScripts.map(renderScriptTag),\r\n ];\r\n if (tags.length === 0) return [];\r\n return [' <!--n-head-->', ...tags, ' <!--/n-head-->'];\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced scripts with position='body', wrapped in\r\n * <!--n-body-scripts-->\u2026<!--/n-body-scripts--> sentinels.\r\n * Injected just before </body> so scripts execute after page content is in the DOM.\r\n * Returns an empty array when there are no body-position scripts.\r\n */\r\nfunction renderManagedBodyScripts(store: HtmlStore): string[] {\r\n const bodyScripts = store.script.filter(s => s.position === 'body');\r\n if (bodyScripts.length === 0) return [];\r\n return [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->'];\r\n}\r\n\r\n// \u2500\u2500\u2500 Main SSR handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a page for the given URL and writes the full HTML response.\r\n *\r\n * @param url The raw request URL (may include query string).\r\n * @param res Node ServerResponse to write to.\r\n * @param pagesDir Absolute path to the app/pages directory.\r\n * @param isDev When true, injects the HMR client script into the page.\r\n */\r\nexport async function serverSideRender(\r\n url: string,\r\n res: ServerResponse,\r\n pagesDir: string,\r\n isDev = false,\r\n): Promise<void> {\r\n const skipClientSSR = url.includes('__hmr=1');\r\n const cleanUrl = url.split('?')[0];\r\n\r\n // \u2500\u2500 Route resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const routeMatch = matchRoute(cleanUrl, pagesDir);\r\n if (!routeMatch) {\r\n log.verbose(`No route found for: ${url}`);\r\n res.statusCode = 404;\r\n res.end('Page not found');\r\n return;\r\n }\r\n\r\n const { filePath, params, routePattern } = routeMatch;\r\n log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // Merge query string params into props so page components can access them\r\n // the same way in dev and production. Route params take precedence so a\r\n // ?slug=x in the query string cannot shadow a [slug] dynamic segment.\r\n const searchParams = new URL(url, 'http://localhost').searchParams;\r\n const queryParams: Record<string, string | string[]> = {};\r\n searchParams.forEach((_, k) => {\r\n if (!(k in params)) {\r\n const all = searchParams.getAll(k);\r\n queryParams[k] = all.length > 1 ? all : all[0];\r\n }\r\n });\r\n const mergedParams = { ...queryParams, ...params };\r\n\r\n // \u2500\u2500 Module import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // tsImport bypasses Node's ESM module cache entirely so edits are reflected\r\n // immediately on every request in dev.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n const { default: PageComponent } = await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n );\r\n const wrappedElement = await wrapWithLayouts(\r\n createElement(PageComponent, mergedParams),\r\n layoutPaths,\r\n );\r\n\r\n // \u2500\u2500 Client component discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Walk the import tree for both the page and its layout chain.\r\n const registry = new Map<string, string>();\r\n for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n\r\n log.verbose(\r\n `Page ${routePattern}: found ${registry.size} client component(s)`,\r\n `[${[...registry.keys()].join(', ')}]`,\r\n );\r\n\r\n // \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const ctx: RenderContext = { registry, hydrated: new Set(), skipClientSSR };\r\n\r\n let appHtml = '';\r\n const store: HtmlStore = await runWithHtmlStore(async () => {\r\n appHtml = await renderElementToHtml(wrappedElement, ctx);\r\n });\r\n\r\n // \u2500\u2500 Head assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const pageTitle = resolveTitle(store.titleOps, 'NukeJS');\r\n\r\n const headLines: string[] = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n ` <title>${escapeAttr(pageTitle)}</title>`,\r\n ...renderManagedHeadTags(store),\r\n ];\r\n\r\n // \u2500\u2500 Runtime data blob \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Escape </script> sequences so the JSON cannot break out of the script tag.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated],\r\n allIds: [...registry.keys()],\r\n url,\r\n params,\r\n debug: toClientDebugLevel(getDebugLevel()),\r\n })\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n\r\n // \u2500\u2500 Body scripts (position='body') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const bodyScriptLines = renderManagedBodyScripts(store);\r\n const bodyScriptsHtml = bodyScriptLines.length > 0\r\n ? '\\n' + bodyScriptLines.join('\\n') + '\\n'\r\n : '';\r\n\r\n // \u2500\u2500 Full document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const html = `<!DOCTYPE html>\r\n${openTag('html', store.htmlAttrs)}\r\n<head>\r\n${headLines.join('\\n')}\r\n</head>\r\n${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n{\r\n \"imports\": {\r\n \"react\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n}\r\n </script>\r\n\r\n <script type=\"module\">\r\n await import('react');\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n initRuntime(data);\r\n </script>\r\n\r\n ${isDev ? '<script type=\"module\" src=\"/__hmr.js\"></script>' : ''}\r\n${bodyScriptsHtml}</body>\r\n</html>`;\r\n\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end(html);\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/use-html.js CHANGED
@@ -79,7 +79,11 @@ function clientUseHtml(options) {
79
79
  if (tag.integrity) el.integrity = tag.integrity;
80
80
  if (tag.content) el.textContent = tag.content;
81
81
  el.dataset.usehtml = id;
82
- document.head.appendChild(el);
82
+ if (tag.position === "body") {
83
+ document.body.appendChild(el);
84
+ } else {
85
+ document.head.appendChild(el);
86
+ }
83
87
  return el;
84
88
  });
85
89
  return () => nodes.forEach((n) => n.remove());
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/use-html.ts"],
4
- "sourcesContent": ["/**\r\n * use-html.ts \u2014 useHtml() Hook\r\n *\r\n * A universal hook that lets React components control the HTML document's\r\n * <head>, <html> attributes, and <body> attributes from within JSX \u2014 on both\r\n * the server (SSR) and the client (hydration / SPA navigation).\r\n *\r\n * Server behaviour:\r\n * Writes directly into the per-request html-store. The store is flushed\r\n * into the HTML document after the component tree is fully rendered.\r\n * useHtml() is called synchronously during rendering so no actual React\r\n * hook is used \u2014 it's just a function that pokes the globalThis store.\r\n *\r\n * Client behaviour:\r\n * Uses useEffect() to apply changes to the live document and clean them up\r\n * when the component unmounts (navigation, unmount). Each effect is keyed\r\n * to its options object via JSON.stringify so React re-runs it when the\r\n * options change.\r\n *\r\n * Layout title templates:\r\n * Layouts typically set title as a function so they can append a site suffix:\r\n *\r\n * ```tsx\r\n * // Root layout\r\n * useHtml({ title: (prev) => `${prev} | Acme` });\r\n *\r\n * // A page\r\n * useHtml({ title: 'About' });\r\n * // \u2192 'About | Acme'\r\n * ```\r\n *\r\n * Example usage:\r\n * ```tsx\r\n * useHtml({\r\n * title: 'Blog Post',\r\n * meta: [{ name: 'description', content: 'A great post' }],\r\n * link: [{ rel: 'canonical', href: 'https://example.com/post' }],\r\n * });\r\n * ```\r\n */\r\n\r\nimport { useEffect } from 'react';\r\nimport { getHtmlStore } from './html-store';\r\nimport type {\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './html-store';\r\n\r\n// Re-export types so consumers can import them from 'nukejs' directly.\r\nexport type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag };\r\n\r\n// \u2500\u2500\u2500 Options type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface HtmlOptions {\r\n /**\r\n * Page title.\r\n * string \u2192 sets the title directly (page wins over layout).\r\n * function \u2192 receives the inner title; use in layouts to append a suffix:\r\n * `(prev) => \\`${prev} | MySite\\``\r\n */\r\n title?: TitleValue;\r\n /** Attributes merged onto <html>. Per-attribute last-write-wins. */\r\n htmlAttrs?: HtmlAttrs;\r\n /** Attributes merged onto <body>. Per-attribute last-write-wins. */\r\n bodyAttrs?: BodyAttrs;\r\n meta?: MetaTag[];\r\n link?: LinkTag[];\r\n script?: ScriptTag[];\r\n style?: StyleTag[];\r\n}\r\n\r\n// \u2500\u2500\u2500 Universal hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Applies HTML document customisations from a React component.\r\n * Automatically detects whether it is running on the server or the client.\r\n */\r\nexport function useHtml(options: HtmlOptions): void {\r\n if (typeof document === 'undefined') {\r\n // Running on the server (SSR) \u2014 write synchronously to the request store.\r\n serverUseHtml(options);\r\n } else {\r\n // Running in the browser \u2014 use React effects.\r\n clientUseHtml(options);\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Server implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Writes options directly into the active per-request html-store.\r\n * Called synchronously during SSR; no React hooks are used.\r\n *\r\n * Title operations are *pushed* (not replaced) so both layout and page values\r\n * are preserved for resolveTitle() to process in the correct order.\r\n */\r\nfunction serverUseHtml(options: HtmlOptions): void {\r\n const store = getHtmlStore();\r\n if (!store) return; // Called outside of a runWithHtmlStore context \u2014 ignore.\r\n\r\n if (options.title !== undefined) store.titleOps.push(options.title);\r\n if (options.htmlAttrs) Object.assign(store.htmlAttrs, options.htmlAttrs);\r\n if (options.bodyAttrs) Object.assign(store.bodyAttrs, options.bodyAttrs);\r\n if (options.meta?.length) store.meta.push(...options.meta);\r\n if (options.link?.length) store.link.push(...options.link);\r\n if (options.script?.length) store.script.push(...options.script);\r\n if (options.style?.length) store.style.push(...options.style);\r\n}\r\n\r\n// \u2500\u2500\u2500 Client implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Monotonically incrementing counter for generating unique dataset IDs. */\r\nlet _uid = 0;\r\nconst uid = () => `uh${++_uid}`;\r\n\r\n/**\r\n * Applies options to the live document using React effects.\r\n * Each effect type is independent so a change to `title` does not re-run the\r\n * `meta` effect and vice versa.\r\n *\r\n * Cleanup functions restore the previous state so unmounting a component that\r\n * called useHtml() reverses its changes (important for SPA navigation).\r\n */\r\nfunction clientUseHtml(options: HtmlOptions): void {\r\n // \u2500\u2500 title \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (options.title === undefined) return;\r\n const prev = document.title;\r\n document.title = typeof options.title === 'function'\r\n ? options.title(prev)\r\n : options.title;\r\n return () => { document.title = prev; };\r\n }, [typeof options.title === 'function' // eslint-disable-line react-hooks/exhaustive-deps\r\n ? options.title.toString()\r\n : options.title]);\r\n\r\n // \u2500\u2500 <html> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.htmlAttrs) return;\r\n return applyAttrs(document.documentElement, options.htmlAttrs);\r\n }, [JSON.stringify(options.htmlAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <body> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.bodyAttrs) return;\r\n return applyAttrs(document.body, options.bodyAttrs);\r\n }, [JSON.stringify(options.bodyAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <meta> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.meta?.length) return;\r\n const id = uid();\r\n const nodes = options.meta.map((tag) => {\r\n const el = document.createElement('meta');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.meta)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <link> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.link?.length) return;\r\n const id = uid();\r\n const nodes = options.link.map((tag) => {\r\n const el = document.createElement('link');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.link)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <script> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.script?.length) return;\r\n const id = uid();\r\n const nodes = options.script.map((tag) => {\r\n const el = document.createElement('script');\r\n if (tag.src) el.src = tag.src;\r\n if (tag.type) el.type = tag.type;\r\n if (tag.defer) el.defer = true;\r\n if (tag.async) el.async = true;\r\n if (tag.noModule) el.setAttribute('nomodule', '');\r\n if (tag.crossOrigin) el.crossOrigin = tag.crossOrigin;\r\n if (tag.integrity) el.integrity = tag.integrity;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.script)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <style> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.style?.length) return;\r\n const id = uid();\r\n const nodes = options.style.map((tag) => {\r\n const el = document.createElement('style');\r\n if (tag.media) el.media = tag.media;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.style)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n}\r\n\r\n// \u2500\u2500\u2500 Attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Applies an attribute map to a DOM element, storing the previous values so\r\n * the returned cleanup function can restore them on unmount.\r\n */\r\nfunction applyAttrs(\r\n el: Element,\r\n attrs: Record<string, string | undefined>,\r\n): () => void {\r\n const prev: Record<string, string | null> = {};\r\n for (const [k, v] of Object.entries(attrs)) {\r\n if (v === undefined) continue;\r\n const attr = domAttr(k);\r\n prev[attr] = el.getAttribute(attr);\r\n el.setAttribute(attr, v);\r\n }\r\n return () => {\r\n for (const [attr, was] of Object.entries(prev)) {\r\n if (was === null) el.removeAttribute(attr);\r\n else el.setAttribute(attr, was);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Converts camelCase React prop names to their HTML attribute equivalents.\r\n * httpEquiv \u2192 http-equiv\r\n * hrefLang \u2192 hreflang\r\n * crossOrigin \u2192 crossorigin\r\n */\r\nfunction domAttr(key: string): string {\r\n if (key === 'httpEquiv') return 'http-equiv';\r\n if (key === 'hrefLang') return 'hreflang';\r\n if (key === 'crossOrigin') return 'crossorigin';\r\n return key;\r\n}\r\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * use-html.ts \u2014 useHtml() Hook\r\n *\r\n * A universal hook that lets React components control the HTML document's\r\n * <head>, <html> attributes, and <body> attributes from within JSX \u2014 on both\r\n * the server (SSR) and the client (hydration / SPA navigation).\r\n *\r\n * Server behaviour:\r\n * Writes directly into the per-request html-store. The store is flushed\r\n * into the HTML document after the component tree is fully rendered.\r\n * useHtml() is called synchronously during rendering so no actual React\r\n * hook is used \u2014 it's just a function that pokes the globalThis store.\r\n *\r\n * Client behaviour:\r\n * Uses useEffect() to apply changes to the live document and clean them up\r\n * when the component unmounts (navigation, unmount). Each effect is keyed\r\n * to its options object via JSON.stringify so React re-runs it when the\r\n * options change.\r\n *\r\n * Layout title templates:\r\n * Layouts typically set title as a function so they can append a site suffix:\r\n *\r\n * ```tsx\r\n * // Root layout\r\n * useHtml({ title: (prev) => `${prev} | Acme` });\r\n *\r\n * // A page\r\n * useHtml({ title: 'About' });\r\n * // \u2192 'About | Acme'\r\n * ```\r\n *\r\n * Example usage:\r\n * ```tsx\r\n * useHtml({\r\n * title: 'Blog Post',\r\n * meta: [{ name: 'description', content: 'A great post' }],\r\n * link: [{ rel: 'canonical', href: 'https://example.com/post' }],\r\n * });\r\n * ```\r\n */\r\n\r\nimport { useEffect } from 'react';\r\nimport { getHtmlStore } from './html-store';\r\nimport type {\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './html-store';\r\n\r\n// Re-export types so consumers can import them from 'nukejs' directly.\r\nexport type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag };\r\n\r\n// \u2500\u2500\u2500 Options type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface HtmlOptions {\r\n /**\r\n * Page title.\r\n * string \u2192 sets the title directly (page wins over layout).\r\n * function \u2192 receives the inner title; use in layouts to append a suffix:\r\n * `(prev) => \\`${prev} | MySite\\``\r\n */\r\n title?: TitleValue;\r\n /** Attributes merged onto <html>. Per-attribute last-write-wins. */\r\n htmlAttrs?: HtmlAttrs;\r\n /** Attributes merged onto <body>. Per-attribute last-write-wins. */\r\n bodyAttrs?: BodyAttrs;\r\n meta?: MetaTag[];\r\n link?: LinkTag[];\r\n script?: ScriptTag[];\r\n style?: StyleTag[];\r\n}\r\n\r\n// \u2500\u2500\u2500 Universal hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Applies HTML document customisations from a React component.\r\n * Automatically detects whether it is running on the server or the client.\r\n */\r\nexport function useHtml(options: HtmlOptions): void {\r\n if (typeof document === 'undefined') {\r\n // Running on the server (SSR) \u2014 write synchronously to the request store.\r\n serverUseHtml(options);\r\n } else {\r\n // Running in the browser \u2014 use React effects.\r\n clientUseHtml(options);\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Server implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Writes options directly into the active per-request html-store.\r\n * Called synchronously during SSR; no React hooks are used.\r\n *\r\n * Title operations are *pushed* (not replaced) so both layout and page values\r\n * are preserved for resolveTitle() to process in the correct order.\r\n */\r\nfunction serverUseHtml(options: HtmlOptions): void {\r\n const store = getHtmlStore();\r\n if (!store) return; // Called outside of a runWithHtmlStore context \u2014 ignore.\r\n\r\n if (options.title !== undefined) store.titleOps.push(options.title);\r\n if (options.htmlAttrs) Object.assign(store.htmlAttrs, options.htmlAttrs);\r\n if (options.bodyAttrs) Object.assign(store.bodyAttrs, options.bodyAttrs);\r\n if (options.meta?.length) store.meta.push(...options.meta);\r\n if (options.link?.length) store.link.push(...options.link);\r\n if (options.script?.length) store.script.push(...options.script);\r\n if (options.style?.length) store.style.push(...options.style);\r\n}\r\n\r\n// \u2500\u2500\u2500 Client implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Monotonically incrementing counter for generating unique dataset IDs. */\r\nlet _uid = 0;\r\nconst uid = () => `uh${++_uid}`;\r\n\r\n/**\r\n * Applies options to the live document using React effects.\r\n * Each effect type is independent so a change to `title` does not re-run the\r\n * `meta` effect and vice versa.\r\n *\r\n * Cleanup functions restore the previous state so unmounting a component that\r\n * called useHtml() reverses its changes (important for SPA navigation).\r\n */\r\nfunction clientUseHtml(options: HtmlOptions): void {\r\n // \u2500\u2500 title \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (options.title === undefined) return;\r\n const prev = document.title;\r\n document.title = typeof options.title === 'function'\r\n ? options.title(prev)\r\n : options.title;\r\n return () => { document.title = prev; };\r\n }, [typeof options.title === 'function' // eslint-disable-line react-hooks/exhaustive-deps\r\n ? options.title.toString()\r\n : options.title]);\r\n\r\n // \u2500\u2500 <html> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.htmlAttrs) return;\r\n return applyAttrs(document.documentElement, options.htmlAttrs);\r\n }, [JSON.stringify(options.htmlAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <body> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.bodyAttrs) return;\r\n return applyAttrs(document.body, options.bodyAttrs);\r\n }, [JSON.stringify(options.bodyAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <meta> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.meta?.length) return;\r\n const id = uid();\r\n const nodes = options.meta.map((tag) => {\r\n const el = document.createElement('meta');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.meta)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <link> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.link?.length) return;\r\n const id = uid();\r\n const nodes = options.link.map((tag) => {\r\n const el = document.createElement('link');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.link)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <script> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.script?.length) return;\r\n const id = uid();\r\n const nodes = options.script.map((tag) => {\r\n const el = document.createElement('script');\r\n if (tag.src) el.src = tag.src;\r\n if (tag.type) el.type = tag.type;\r\n if (tag.defer) el.defer = true;\r\n if (tag.async) el.async = true;\r\n if (tag.noModule) el.setAttribute('nomodule', '');\r\n if (tag.crossOrigin) el.crossOrigin = tag.crossOrigin;\r\n if (tag.integrity) el.integrity = tag.integrity;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n // Respect position: 'body' scripts are appended at the end of <body>.\r\n if (tag.position === 'body') {\r\n document.body.appendChild(el);\r\n } else {\r\n document.head.appendChild(el);\r\n }\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.script)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <style> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.style?.length) return;\r\n const id = uid();\r\n const nodes = options.style.map((tag) => {\r\n const el = document.createElement('style');\r\n if (tag.media) el.media = tag.media;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.style)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n}\r\n\r\n// \u2500\u2500\u2500 Attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Applies an attribute map to a DOM element, storing the previous values so\r\n * the returned cleanup function can restore them on unmount.\r\n */\r\nfunction applyAttrs(\r\n el: Element,\r\n attrs: Record<string, string | undefined>,\r\n): () => void {\r\n const prev: Record<string, string | null> = {};\r\n for (const [k, v] of Object.entries(attrs)) {\r\n if (v === undefined) continue;\r\n const attr = domAttr(k);\r\n prev[attr] = el.getAttribute(attr);\r\n el.setAttribute(attr, v);\r\n }\r\n return () => {\r\n for (const [attr, was] of Object.entries(prev)) {\r\n if (was === null) el.removeAttribute(attr);\r\n else el.setAttribute(attr, was);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Converts camelCase React prop names to their HTML attribute equivalents.\r\n * httpEquiv \u2192 http-equiv\r\n * hrefLang \u2192 hreflang\r\n * crossOrigin \u2192 crossorigin\r\n */\r\nfunction domAttr(key: string): string {\r\n if (key === 'httpEquiv') return 'http-equiv';\r\n if (key === 'hrefLang') return 'hreflang';\r\n if (key === 'crossOrigin') return 'crossorigin';\r\n return key;\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -25,4 +25,4 @@ function useRouter() {
25
25
  export {
26
26
  useRouter as default
27
27
  };
28
- //# sourceMappingURL=useRouter.js.map
28
+ //# sourceMappingURL=use-router.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/as-is/useRouter.ts"],
4
- "sourcesContent": ["import { useCallback, useEffect, useState } from \"react\";\r\n\r\ntype Router = {\r\n path: string;\r\n push: (url: string) => void;\r\n replace: (url: string) => void;\r\n};\r\n\r\nexport default function useRouter(): Router {\r\n try {\r\n const [path, setPath] = useState(() => window.location.pathname);\r\n\r\n useEffect(() => {\r\n const handleLocationChange = () => setPath(window.location.pathname);\r\n window.addEventListener(\"locationchange\", handleLocationChange);\r\n return () => window.removeEventListener(\"locationchange\", handleLocationChange);\r\n }, []);\r\n\r\n const push = useCallback((url: string) => {\r\n window.history.pushState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n const replace = useCallback((url: string) => {\r\n window.history.replaceState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n return { path, push, replace };\r\n } catch {\r\n return { push: () => {}, replace: () => {}, path: \"\" };\r\n }\r\n}\r\n"],
3
+ "sources": ["../src/use-router.ts"],
4
+ "sourcesContent": ["import { useCallback, useEffect, useState } from \"react\";\r\n\r\ntype Router = {\r\n path: string;\r\n push: (url: string) => void;\r\n replace: (url: string) => void;\r\n};\r\n\r\nexport default function useRouter(): Router {\r\n try {\r\n const [path, setPath] = useState(() => window.location.pathname);\r\n\r\n useEffect(() => {\r\n const handleLocationChange = () => setPath(window.location.pathname);\r\n window.addEventListener(\"locationchange\", handleLocationChange);\r\n return () => window.removeEventListener(\"locationchange\", handleLocationChange);\r\n }, []);\r\n\r\n const push = useCallback((url: string) => {\r\n window.history.pushState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n const replace = useCallback((url: string) => {\r\n window.history.replaceState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n return { path, push, replace };\r\n } catch {\r\n return { push: () => {}, replace: () => {}, path: \"\" };\r\n }\r\n}"],
5
5
  "mappings": "AAAA,SAAS,aAAa,WAAW,gBAAgB;AAQlC,SAAR,YAAqC;AACxC,MAAI;AACA,UAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,QAAQ;AAE/D,cAAU,MAAM;AACZ,YAAM,uBAAuB,MAAM,QAAQ,OAAO,SAAS,QAAQ;AACnE,aAAO,iBAAiB,kBAAkB,oBAAoB;AAC9D,aAAO,MAAM,OAAO,oBAAoB,kBAAkB,oBAAoB;AAAA,IAClF,GAAG,CAAC,CAAC;AAEL,UAAM,OAAO,YAAY,CAAC,QAAgB;AACtC,aAAO,QAAQ,UAAU,CAAC,GAAG,IAAI,GAAG;AACpC,cAAQ,GAAG;AAAA,IACf,GAAG,CAAC,CAAC;AAEL,UAAM,UAAU,YAAY,CAAC,QAAgB;AACzC,aAAO,QAAQ,aAAa,CAAC,GAAG,IAAI,GAAG;AACvC,cAAQ,GAAG;AAAA,IACf,GAAG,CAAC,CAAC;AAEL,WAAO,EAAE,MAAM,MAAM,QAAQ;AAAA,EACjC,QAAQ;AACJ,WAAO,EAAE,MAAM,MAAM;AAAA,IAAC,GAAG,SAAS,MAAM;AAAA,IAAC,GAAG,MAAM,GAAG;AAAA,EACzD;AACJ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/as-is/Link.tsx"],
4
- "sourcesContent": ["\"use client\"\r\n\r\nconst Link = ({ href, children, className }: {\r\n href: string;\r\n children: React.ReactNode;\r\n className?: string;\r\n}) => {\r\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\r\n e.preventDefault();\r\n window.history.pushState({}, '', href);\r\n };\r\n\r\n return (\r\n <a href={href} onClick={handleClick} className={className}>\r\n {children}\r\n </a>\r\n );\r\n};\r\n\r\nexport default Link;\r\n"],
5
- "mappings": ";AAaQ;AAXR,MAAM,OAAO,CAAC,EAAE,MAAM,UAAU,UAAU,MAIpC;AACF,QAAM,cAAc,CAAC,MAA2C;AAC5D,MAAE,eAAe;AACjB,WAAO,QAAQ,UAAU,CAAC,GAAG,IAAI,IAAI;AAAA,EACzC;AAEA,SACI,oBAAC,OAAE,MAAY,SAAS,aAAa,WAChC,UACL;AAER;AAEA,IAAO,eAAQ;",
6
- "names": []
7
- }
@@ -1,20 +0,0 @@
1
- "use client"
2
-
3
- const Link = ({ href, children, className }: {
4
- href: string;
5
- children: React.ReactNode;
6
- className?: string;
7
- }) => {
8
- const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
9
- e.preventDefault();
10
- window.history.pushState({}, '', href);
11
- };
12
-
13
- return (
14
- <a href={href} onClick={handleClick} className={className}>
15
- {children}
16
- </a>
17
- );
18
- };
19
-
20
- export default Link;
@@ -1,33 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
-
3
- type Router = {
4
- path: string;
5
- push: (url: string) => void;
6
- replace: (url: string) => void;
7
- };
8
-
9
- export default function useRouter(): Router {
10
- try {
11
- const [path, setPath] = useState(() => window.location.pathname);
12
-
13
- useEffect(() => {
14
- const handleLocationChange = () => setPath(window.location.pathname);
15
- window.addEventListener("locationchange", handleLocationChange);
16
- return () => window.removeEventListener("locationchange", handleLocationChange);
17
- }, []);
18
-
19
- const push = useCallback((url: string) => {
20
- window.history.pushState({}, "", url);
21
- setPath(url);
22
- }, []);
23
-
24
- const replace = useCallback((url: string) => {
25
- window.history.replaceState({}, "", url);
26
- setPath(url);
27
- }, []);
28
-
29
- return { path, push, replace };
30
- } catch {
31
- return { push: () => {}, replace: () => {}, path: "" };
32
- }
33
- }
File without changes
File without changes