nukejs 0.0.1 → 0.0.2

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +529 -0
  3. package/bin/index.mjs +126 -0
  4. package/dist/app.d.ts +18 -0
  5. package/dist/app.js +124 -0
  6. package/dist/app.js.map +7 -0
  7. package/dist/as-is/Link.d.ts +6 -0
  8. package/dist/as-is/Link.tsx +20 -0
  9. package/dist/as-is/useRouter.d.ts +7 -0
  10. package/dist/as-is/useRouter.ts +33 -0
  11. package/dist/build-common.d.ts +192 -0
  12. package/dist/build-common.js +737 -0
  13. package/dist/build-common.js.map +7 -0
  14. package/dist/build-node.d.ts +1 -0
  15. package/dist/build-node.js +170 -0
  16. package/dist/build-node.js.map +7 -0
  17. package/dist/build-vercel.d.ts +1 -0
  18. package/dist/build-vercel.js +65 -0
  19. package/dist/build-vercel.js.map +7 -0
  20. package/dist/builder.d.ts +1 -0
  21. package/dist/builder.js +97 -0
  22. package/dist/builder.js.map +7 -0
  23. package/dist/bundle.d.ts +68 -0
  24. package/dist/bundle.js +166 -0
  25. package/dist/bundle.js.map +7 -0
  26. package/dist/bundler.d.ts +58 -0
  27. package/dist/bundler.js +98 -0
  28. package/dist/bundler.js.map +7 -0
  29. package/dist/component-analyzer.d.ts +72 -0
  30. package/dist/component-analyzer.js +102 -0
  31. package/dist/component-analyzer.js.map +7 -0
  32. package/dist/config.d.ts +35 -0
  33. package/dist/config.js +30 -0
  34. package/dist/config.js.map +7 -0
  35. package/dist/hmr-bundle.d.ts +25 -0
  36. package/dist/hmr-bundle.js +76 -0
  37. package/dist/hmr-bundle.js.map +7 -0
  38. package/dist/hmr.d.ts +55 -0
  39. package/dist/hmr.js +62 -0
  40. package/dist/hmr.js.map +7 -0
  41. package/dist/html-store.d.ts +121 -0
  42. package/dist/html-store.js +42 -0
  43. package/dist/html-store.js.map +7 -0
  44. package/dist/http-server.d.ts +99 -0
  45. package/dist/http-server.js +166 -0
  46. package/dist/http-server.js.map +7 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.js +20 -0
  49. package/dist/index.js.map +7 -0
  50. package/dist/logger.d.ts +58 -0
  51. package/dist/logger.js +53 -0
  52. package/dist/logger.js.map +7 -0
  53. package/dist/metadata.d.ts +50 -0
  54. package/dist/metadata.js +43 -0
  55. package/dist/metadata.js.map +7 -0
  56. package/dist/middleware-loader.d.ts +50 -0
  57. package/dist/middleware-loader.js +50 -0
  58. package/dist/middleware-loader.js.map +7 -0
  59. package/dist/middleware.d.ts +22 -0
  60. package/dist/middleware.example.d.ts +8 -0
  61. package/dist/middleware.example.js +58 -0
  62. package/dist/middleware.example.js.map +7 -0
  63. package/dist/middleware.js +59 -0
  64. package/dist/middleware.js.map +7 -0
  65. package/dist/renderer.d.ts +44 -0
  66. package/dist/renderer.js +130 -0
  67. package/dist/renderer.js.map +7 -0
  68. package/dist/router.d.ts +84 -0
  69. package/dist/router.js +104 -0
  70. package/dist/router.js.map +7 -0
  71. package/dist/ssr.d.ts +39 -0
  72. package/dist/ssr.js +168 -0
  73. package/dist/ssr.js.map +7 -0
  74. package/dist/use-html.d.ts +64 -0
  75. package/dist/use-html.js +125 -0
  76. package/dist/use-html.js.map +7 -0
  77. package/dist/utils.d.ts +26 -0
  78. package/dist/utils.js +62 -0
  79. package/dist/utils.js.map +7 -0
  80. package/package.json +64 -12
package/dist/router.js ADDED
@@ -0,0 +1,104 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ function findAllRoutes(dir, baseDir = dir) {
4
+ if (!fs.existsSync(dir)) return [];
5
+ const routes = [];
6
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
7
+ const fullPath = path.join(dir, entry.name);
8
+ if (entry.isDirectory()) {
9
+ routes.push(...findAllRoutes(fullPath, baseDir));
10
+ } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
11
+ routes.push(path.relative(baseDir, fullPath).replace(/\.(tsx|ts)$/, ""));
12
+ }
13
+ }
14
+ return routes;
15
+ }
16
+ function matchDynamicRoute(urlSegments, routePath) {
17
+ const routeSegments = routePath.split(path.sep);
18
+ if (routeSegments.at(-1) === "index") routeSegments.pop();
19
+ const params = {};
20
+ let ri = 0;
21
+ let ui = 0;
22
+ while (ri < routeSegments.length) {
23
+ const seg = routeSegments[ri];
24
+ const optCatchAll = seg.match(/^\[\[\.\.\.(.+)\]\]$/);
25
+ if (optCatchAll) {
26
+ params[optCatchAll[1]] = urlSegments.slice(ui);
27
+ return { params };
28
+ }
29
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
30
+ if (catchAll) {
31
+ const remaining = urlSegments.slice(ui);
32
+ if (!remaining.length) return null;
33
+ params[catchAll[1]] = remaining;
34
+ return { params };
35
+ }
36
+ const dynamic = seg.match(/^\[(.+)\]$/);
37
+ if (dynamic) {
38
+ if (ui >= urlSegments.length) return null;
39
+ params[dynamic[1]] = urlSegments[ui++];
40
+ ri++;
41
+ continue;
42
+ }
43
+ if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;
44
+ ui++;
45
+ ri++;
46
+ }
47
+ return ui < urlSegments.length ? null : { params };
48
+ }
49
+ function getRouteSpecificity(routePath) {
50
+ return routePath.split(path.sep).reduce((score, seg) => {
51
+ if (seg.match(/^\[\[\.\.\.(.+)\]\]$/)) return score + 1;
52
+ if (seg.match(/^\[\.\.\.(.+)\]$/)) return score + 2;
53
+ if (seg.match(/^\[(.+)\]$/)) return score + 3;
54
+ return score + 4;
55
+ }, 0);
56
+ }
57
+ function isWithinBase(baseDir, filePath) {
58
+ const rel = path.relative(baseDir, filePath);
59
+ return Boolean(rel) && !rel.startsWith("..") && !path.isAbsolute(rel);
60
+ }
61
+ function matchRoute(urlPath, baseDir, extension = ".tsx") {
62
+ const rawSegments = urlPath === "/" ? [] : urlPath.slice(1).split("/");
63
+ if (rawSegments.some((s) => s === ".." || s === ".")) return null;
64
+ const segments = rawSegments.length === 0 ? ["index"] : rawSegments;
65
+ const exactPath = path.join(baseDir, ...segments) + extension;
66
+ if (!isWithinBase(baseDir, exactPath)) return null;
67
+ if (fs.existsSync(exactPath)) {
68
+ return { filePath: exactPath, params: {}, routePattern: segments.join("/") };
69
+ }
70
+ const sortedRoutes = findAllRoutes(baseDir).sort(
71
+ (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a)
72
+ );
73
+ for (const route of sortedRoutes) {
74
+ const match = matchDynamicRoute(segments, route);
75
+ if (!match) continue;
76
+ const filePath = path.join(baseDir, route) + extension;
77
+ if (!isWithinBase(baseDir, filePath)) continue;
78
+ if (fs.existsSync(filePath)) {
79
+ return { filePath, params: match.params, routePattern: route };
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+ function findLayoutsForRoute(routeFilePath, pagesDir) {
85
+ const layouts = [];
86
+ const rootLayout = path.join(pagesDir, "layout.tsx");
87
+ if (fs.existsSync(rootLayout)) layouts.push(rootLayout);
88
+ const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));
89
+ if (!relativePath || relativePath === ".") return layouts;
90
+ const segments = relativePath.split(path.sep).filter((s) => s !== ".");
91
+ for (let i = 1; i <= segments.length; i++) {
92
+ const layoutPath = path.join(pagesDir, ...segments.slice(0, i), "layout.tsx");
93
+ if (fs.existsSync(layoutPath)) layouts.push(layoutPath);
94
+ }
95
+ return layouts;
96
+ }
97
+ export {
98
+ findAllRoutes,
99
+ findLayoutsForRoute,
100
+ getRouteSpecificity,
101
+ matchDynamicRoute,
102
+ matchRoute
103
+ };
104
+ //# sourceMappingURL=router.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/router.ts"],
4
+ "sourcesContent": ["/**\r\n * router.ts \u2014 File-System Based URL Router\r\n *\r\n * Maps incoming URL paths to handler files using Next.js-compatible conventions:\r\n *\r\n * server/users/index.ts \u2192 /users\r\n * server/users/[id].ts \u2192 /users/:id (dynamic segment)\r\n * server/blog/[...slug].ts \u2192 /blog/* (catch-all)\r\n * server/files/[[...path]].ts \u2192 /files or /files/* (optional catch-all)\r\n *\r\n * Route specificity (higher = wins over lower):\r\n * static segment +4 (e.g. 'about')\r\n * dynamic segment +3 (e.g. '[id]')\r\n * catch-all +2 (e.g. '[...slug]')\r\n * optional catch-all +1 (e.g. '[[...path]]')\r\n *\r\n * Path traversal protection:\r\n * matchRoute() rejects URL segments that contain '..' or '.' and verifies\r\n * that the resolved file path stays inside the base directory before\r\n * checking whether the file exists.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\n\r\n// \u2500\u2500\u2500 Route file 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively collects all .ts/.tsx files in `dir`, returning paths relative to\r\n * `baseDir` without the file extension.\r\n *\r\n * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']\r\n */\r\nexport function findAllRoutes(dir: string, baseDir: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n\r\n const routes: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const fullPath = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n routes.push(...findAllRoutes(fullPath, baseDir));\r\n } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {\r\n routes.push(path.relative(baseDir, fullPath).replace(/\\.(tsx|ts)$/, ''));\r\n }\r\n }\r\n return routes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic segment matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Attempts to match `urlSegments` against a route that may contain dynamic\r\n * segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).\r\n *\r\n * Returns the captured params on success, or null if the route does not match.\r\n *\r\n * Param value types:\r\n * [param] \u2192 string\r\n * [...slug] \u2192 string[] (at least one segment required)\r\n * [[...path]] \u2192 string[] (zero or more segments)\r\n */\r\nexport function matchDynamicRoute(\r\n urlSegments: string[],\r\n routePath: string,\r\n): { params: Record<string, string | string[]> } | null {\r\n const routeSegments = routePath.split(path.sep);\r\n\r\n // 'index' at the end of a route path means the route handles the parent directory URL.\r\n if (routeSegments.at(-1) === 'index') routeSegments.pop();\r\n\r\n const params: Record<string, string | string[]> = {};\r\n let ri = 0; // route segment index\r\n let ui = 0; // URL segment index\r\n\r\n while (ri < routeSegments.length) {\r\n const seg = routeSegments[ri];\r\n\r\n // [[...name]] \u2014 optional catch-all: consumes zero or more remaining URL segments.\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n params[optCatchAll[1]] = urlSegments.slice(ui);\r\n return { params };\r\n }\r\n\r\n // [...name] \u2014 required catch-all: must consume at least one URL segment.\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n const remaining = urlSegments.slice(ui);\r\n if (!remaining.length) return null;\r\n params[catchAll[1]] = remaining;\r\n return { params };\r\n }\r\n\r\n // [name] \u2014 single dynamic segment: consumes exactly one URL segment.\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n if (ui >= urlSegments.length) return null;\r\n params[dynamic[1]] = urlSegments[ui++];\r\n ri++;\r\n continue;\r\n }\r\n\r\n // Static segment \u2014 must match exactly.\r\n if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;\r\n ui++; ri++;\r\n }\r\n\r\n // All route segments consumed \u2014 URL must be fully consumed too.\r\n return ui < urlSegments.length ? null : { params };\r\n}\r\n\r\n// \u2500\u2500\u2500 Specificity scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Computes a specificity score for a route path.\r\n * Used to sort candidate routes so more specific routes shadow catch-alls.\r\n *\r\n * Higher score = more specific:\r\n * static segment 4\r\n * [dynamic] 3\r\n * [...catchAll] 2\r\n * [[...optCatchAll]] 1\r\n */\r\nexport function getRouteSpecificity(routePath: string): number {\r\n return routePath.split(path.sep).reduce((score, seg) => {\r\n if (seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)) return score + 1;\r\n if (seg.match(/^\\[\\.\\.\\.(.+)\\]$/)) return score + 2;\r\n if (seg.match(/^\\[(.+)\\]$/)) return score + 3;\r\n return score + 4; // static segment\r\n }, 0);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route match result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 RouteMatch {\r\n filePath: string;\r\n params: Record<string, string | string[]>;\r\n routePattern: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 Path traversal guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Returns true only when `filePath` is a descendant of `baseDir`.\r\n * Used to prevent URL path traversal attacks (e.g. /../../etc/passwd).\r\n */\r\nfunction isWithinBase(baseDir: string, filePath: string): boolean {\r\n const rel = path.relative(baseDir, filePath);\r\n return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Resolves a URL path to a route file inside `baseDir`.\r\n *\r\n * Steps:\r\n * 1. Reject '..' or '.' path segments (path traversal guard).\r\n * 2. Try an exact file match (e.g. /about \u2192 baseDir/about.tsx).\r\n * 3. Sort all discovered routes by specificity (most specific first).\r\n * 4. Return the first dynamic route that matches.\r\n *\r\n * @param urlPath The URL path to match (e.g. '/users/42').\r\n * @param baseDir Absolute path to the directory containing route files.\r\n * @param extension File extension to look for ('.tsx' or '.ts').\r\n */\r\nexport function matchRoute(\r\n urlPath: string,\r\n baseDir: string,\r\n extension = '.tsx',\r\n): RouteMatch | null {\r\n // Split the URL path into segments, rejecting any that attempt path traversal.\r\n const rawSegments = urlPath === '/' ? [] : urlPath.slice(1).split('/');\r\n if (rawSegments.some(s => s === '..' || s === '.')) return null;\r\n\r\n // For the root URL, look for an index file.\r\n const segments = rawSegments.length === 0 ? ['index'] : rawSegments;\r\n\r\n // 1. Exact match: /about \u2192 about.tsx\r\n const exactPath = path.join(baseDir, ...segments) + extension;\r\n if (!isWithinBase(baseDir, exactPath)) return null;\r\n if (fs.existsSync(exactPath)) {\r\n return { filePath: exactPath, params: {}, routePattern: segments.join('/') };\r\n }\r\n\r\n // 2. Dynamic match \u2014 try routes sorted by specificity so '[id]' wins over '[...all]'.\r\n const sortedRoutes = findAllRoutes(baseDir).sort(\r\n (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a),\r\n );\r\n\r\n for (const route of sortedRoutes) {\r\n const match = matchDynamicRoute(segments, route);\r\n if (!match) continue;\r\n const filePath = path.join(baseDir, route) + extension;\r\n if (!isWithinBase(baseDir, filePath)) continue;\r\n if (fs.existsSync(filePath)) {\r\n return { filePath, params: match.params, routePattern: route };\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns every layout.tsx file that wraps a given route file, in\r\n * outermost-first order (root layout first, nearest layout last).\r\n *\r\n * Layout chain example for app/pages/blog/[slug]/page.tsx:\r\n * app/pages/layout.tsx \u2190 root layout\r\n * app/pages/blog/layout.tsx \u2190 blog section layout\r\n *\r\n * The outermost-first order matches how wrapWithLayouts() nests them:\r\n * the last layout in the array is the innermost wrapper.\r\n */\r\nexport function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n // Root layout wraps everything.\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n // Walk the directory hierarchy from pagesDir to the file's parent.\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(s => s !== '.');\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}\r\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/ssr.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ssr.ts — Server-Side Rendering Pipeline (Dev Mode)
3
+ *
4
+ * Handles the full page render cycle for `nuke dev`:
5
+ *
6
+ * 1. Match the incoming URL to a page file in app/pages using the file-system
7
+ * router.
8
+ * 2. Discover the layout chain (layout.tsx files from root to the page dir).
9
+ * 3. Dynamically import the page and layout modules (always fresh via ?t=Date.now()).
10
+ * 4. Walk the import tree to discover all "use client" components.
11
+ * 5. Render the wrapped element tree with the async renderer.
12
+ * 6. Flush the html-store (title, meta, link, script, style tags).
13
+ * 7. Assemble and send the full HTML document including:
14
+ * - The rendered app HTML
15
+ * - An importmap pointing react/nukejs to the bundled versions
16
+ * - A __n_data JSON blob with hydration IDs and runtime config
17
+ * - An inline bootstrap <script> that calls initRuntime()
18
+ * - (dev only) A /__hmr.js script for Hot Module Replacement
19
+ *
20
+ * In production, a pre-built standalone handler (generated by build-common.ts)
21
+ * handles each page without dynamic imports or file-system access.
22
+ *
23
+ * HMR fast path:
24
+ * When the request URL contains `__hmr=1` (added by the HMR client during
25
+ * soft navigation), the renderer skips client-component renderToString
26
+ * (ctx.skipClientSSR = true). This significantly speeds up HMR reloads
27
+ * because the client already has the DOM in place and only needs the new
28
+ * server-rendered markup.
29
+ */
30
+ import type { ServerResponse } from 'http';
31
+ /**
32
+ * Renders a page for the given URL and writes the full HTML response.
33
+ *
34
+ * @param url The raw request URL (may include query string).
35
+ * @param res Node ServerResponse to write to.
36
+ * @param pagesDir Absolute path to the app/pages directory.
37
+ * @param isDev When true, injects the HMR client script into the page.
38
+ */
39
+ export declare function serverSideRender(url: string, res: ServerResponse, pagesDir: string, isDev?: boolean): Promise<void>;
package/dist/ssr.js ADDED
@@ -0,0 +1,168 @@
1
+ import path from "path";
2
+ import { createElement } from "react";
3
+ import { pathToFileURL } from "url";
4
+ import { tsImport } from "tsx/esm/api";
5
+ import { log, getDebugLevel } from "./logger.js";
6
+ import { matchRoute, findLayoutsForRoute } from "./router.js";
7
+ import { findClientComponentsInTree } from "./component-analyzer.js";
8
+ import { renderElementToHtml } from "./renderer.js";
9
+ import {
10
+ runWithHtmlStore,
11
+ resolveTitle
12
+ } from "./html-store.js";
13
+ async function wrapWithLayouts(pageElement, layoutPaths) {
14
+ let element = pageElement;
15
+ for (let i = layoutPaths.length - 1; i >= 0; i--) {
16
+ const { default: LayoutComponent } = await tsImport(
17
+ pathToFileURL(layoutPaths[i]).href,
18
+ { parentURL: import.meta.url }
19
+ );
20
+ element = createElement(LayoutComponent, { children: element });
21
+ }
22
+ return element;
23
+ }
24
+ function toClientDebugLevel(level) {
25
+ if (level === true) return "verbose";
26
+ if (level === "info") return "info";
27
+ if (level === "error") return "error";
28
+ return "silent";
29
+ }
30
+ function escapeAttr(str) {
31
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
32
+ }
33
+ function renderAttrs(attrs) {
34
+ return Object.entries(attrs).filter(([, v]) => v !== void 0 && v !== false).map(([k, v]) => v === true ? k : `${k}="${escapeAttr(String(v))}"`).join(" ");
35
+ }
36
+ function openTag(tag, attrs) {
37
+ const str = renderAttrs(attrs);
38
+ return str ? `<${tag} ${str}>` : `<${tag}>`;
39
+ }
40
+ function metaKey(k) {
41
+ return k === "httpEquiv" ? "http-equiv" : k;
42
+ }
43
+ function linkKey(k) {
44
+ if (k === "hrefLang") return "hreflang";
45
+ if (k === "crossOrigin") return "crossorigin";
46
+ return k;
47
+ }
48
+ function renderMetaTag(tag) {
49
+ const attrs = {};
50
+ for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[metaKey(k)] = v;
51
+ return ` <meta ${renderAttrs(attrs)} />`;
52
+ }
53
+ function renderLinkTag(tag) {
54
+ const attrs = {};
55
+ for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[linkKey(k)] = v;
56
+ return ` <link ${renderAttrs(attrs)} />`;
57
+ }
58
+ function renderScriptTag(tag) {
59
+ const attrs = {
60
+ src: tag.src,
61
+ type: tag.type,
62
+ crossorigin: tag.crossOrigin,
63
+ integrity: tag.integrity,
64
+ defer: tag.defer,
65
+ async: tag.async,
66
+ nomodule: tag.noModule
67
+ };
68
+ const attrStr = renderAttrs(attrs);
69
+ const open = attrStr ? `<script ${attrStr}>` : "<script>";
70
+ return ` ${open}${tag.src ? "" : tag.content ?? ""}</script>`;
71
+ }
72
+ function renderStyleTag(tag) {
73
+ const media = tag.media ? ` media="${escapeAttr(tag.media)}"` : "";
74
+ return ` <style${media}>${tag.content ?? ""}</style>`;
75
+ }
76
+ async function serverSideRender(url, res, pagesDir, isDev = false) {
77
+ const skipClientSSR = url.includes("__hmr=1");
78
+ const cleanUrl = url.split("?")[0];
79
+ const routeMatch = matchRoute(cleanUrl, pagesDir);
80
+ if (!routeMatch) {
81
+ log.verbose(`No route found for: ${url}`);
82
+ res.statusCode = 404;
83
+ res.end("Page not found");
84
+ return;
85
+ }
86
+ const { filePath, params, routePattern } = routeMatch;
87
+ log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);
88
+ const layoutPaths = findLayoutsForRoute(filePath, pagesDir);
89
+ const { default: PageComponent } = await tsImport(
90
+ pathToFileURL(filePath).href,
91
+ { parentURL: import.meta.url }
92
+ );
93
+ const wrappedElement = await wrapWithLayouts(
94
+ createElement(PageComponent, params),
95
+ layoutPaths
96
+ );
97
+ const registry = /* @__PURE__ */ new Map();
98
+ for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))
99
+ registry.set(id, p);
100
+ for (const layoutPath of layoutPaths)
101
+ for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
102
+ registry.set(id, p);
103
+ log.verbose(
104
+ `Page ${routePattern}: found ${registry.size} client component(s)`,
105
+ `[${[...registry.keys()].join(", ")}]`
106
+ );
107
+ const ctx = { registry, hydrated: /* @__PURE__ */ new Set(), skipClientSSR };
108
+ let appHtml = "";
109
+ const store = await runWithHtmlStore(async () => {
110
+ appHtml = await renderElementToHtml(wrappedElement, ctx);
111
+ });
112
+ const pageTitle = resolveTitle(store.titleOps, "SSR App");
113
+ const headLines = [
114
+ ' <meta charset="utf-8" />',
115
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
116
+ ` <title>${escapeAttr(pageTitle)}</title>`,
117
+ ...store.meta.map(renderMetaTag),
118
+ ...store.link.map(renderLinkTag),
119
+ ...store.style.map(renderStyleTag),
120
+ ...store.script.map(renderScriptTag)
121
+ ];
122
+ const runtimeData = JSON.stringify({
123
+ hydrateIds: [...ctx.hydrated],
124
+ // only components actually rendered
125
+ allIds: [...registry.keys()],
126
+ // all reachable — pre-load for SPA nav
127
+ url,
128
+ params,
129
+ debug: toClientDebugLevel(getDebugLevel())
130
+ }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
131
+ const html = `<!DOCTYPE html>
132
+ ${openTag("html", store.htmlAttrs)}
133
+ <head>
134
+ ${headLines.join("\n")}
135
+ </head>
136
+ ${openTag("body", store.bodyAttrs)}
137
+ <div id="app">${appHtml}</div>
138
+
139
+ <script id="__n_data" type="application/json">${runtimeData}</script>
140
+
141
+ <script type="importmap">
142
+ {
143
+ "imports": {
144
+ "react": "/__react.js",
145
+ "react-dom/client": "/__react.js",
146
+ "react/jsx-runtime": "/__react.js",
147
+ "nukejs": "/__n.js"
148
+ }
149
+ }
150
+ </script>
151
+
152
+ <script type="module">
153
+ await import('react');
154
+ const { initRuntime } = await import('nukejs');
155
+ const data = JSON.parse(document.getElementById('__n_data').textContent);
156
+ initRuntime(data);
157
+ </script>
158
+
159
+ ${isDev ? '<script type="module" src="/__hmr.js"></script>' : ""}
160
+ </body>
161
+ </html>`;
162
+ res.setHeader("Content-Type", "text/html");
163
+ res.end(html);
164
+ }
165
+ export {
166
+ serverSideRender
167
+ };
168
+ //# sourceMappingURL=ssr.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 ?t=Date.now()).\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 significantly speeds up HMR reloads\r\n * because the client already has the DOM in place and only needs the new\r\n * server-rendered markup.\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 * Cache-busting (?t=Date.now()) forces Node to re-import the module on every\r\n * request in dev, so editing a layout is reflected immediately.\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 attribute serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\n/** httpEquiv \u2192 http-equiv for proper HTML attribute serialization. */\r\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\n/** hrefLang \u2192 hreflang, crossOrigin \u2192 crossorigin. */\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// \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 // Strip __hmr=1 flag from the URL before routing; set skipClientSSR accordingly.\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 // ?t=Date.now() bypasses Node's module cache so edits are reflected immediately.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n // tsImport creates a fresh isolated module namespace per call \u2014 all static\r\n // imports within the page (layouts, server components, utilities) are loaded\r\n // fresh from disk, bypassing Node's ESM module cache entirely.\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 to collect every\r\n // \"use client\" component that might be rendered.\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, 'SSR App');\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 ...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\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>` so the embedded JSON cannot break out of the script tag.\r\n // JSON parsers accept Unicode escapes transparently.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated], // only components actually rendered\r\n allIds: [...registry.keys()], // all reachable \u2014 pre-load for SPA nav\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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * use-html.ts — useHtml() Hook
3
+ *
4
+ * A universal hook that lets React components control the HTML document's
5
+ * <head>, <html> attributes, and <body> attributes from within JSX — on both
6
+ * the server (SSR) and the client (hydration / SPA navigation).
7
+ *
8
+ * Server behaviour:
9
+ * Writes directly into the per-request html-store. The store is flushed
10
+ * into the HTML document after the component tree is fully rendered.
11
+ * useHtml() is called synchronously during rendering so no actual React
12
+ * hook is used — it's just a function that pokes the globalThis store.
13
+ *
14
+ * Client behaviour:
15
+ * Uses useEffect() to apply changes to the live document and clean them up
16
+ * when the component unmounts (navigation, unmount). Each effect is keyed
17
+ * to its options object via JSON.stringify so React re-runs it when the
18
+ * options change.
19
+ *
20
+ * Layout title templates:
21
+ * Layouts typically set title as a function so they can append a site suffix:
22
+ *
23
+ * ```tsx
24
+ * // Root layout
25
+ * useHtml({ title: (prev) => `${prev} | Acme` });
26
+ *
27
+ * // A page
28
+ * useHtml({ title: 'About' });
29
+ * // → 'About | Acme'
30
+ * ```
31
+ *
32
+ * Example usage:
33
+ * ```tsx
34
+ * useHtml({
35
+ * title: 'Blog Post',
36
+ * meta: [{ name: 'description', content: 'A great post' }],
37
+ * link: [{ rel: 'canonical', href: 'https://example.com/post' }],
38
+ * });
39
+ * ```
40
+ */
41
+ import type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag } from './html-store';
42
+ export type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag };
43
+ export interface HtmlOptions {
44
+ /**
45
+ * Page title.
46
+ * string → sets the title directly (page wins over layout).
47
+ * function → receives the inner title; use in layouts to append a suffix:
48
+ * `(prev) => \`${prev} | MySite\``
49
+ */
50
+ title?: TitleValue;
51
+ /** Attributes merged onto <html>. Per-attribute last-write-wins. */
52
+ htmlAttrs?: HtmlAttrs;
53
+ /** Attributes merged onto <body>. Per-attribute last-write-wins. */
54
+ bodyAttrs?: BodyAttrs;
55
+ meta?: MetaTag[];
56
+ link?: LinkTag[];
57
+ script?: ScriptTag[];
58
+ style?: StyleTag[];
59
+ }
60
+ /**
61
+ * Applies HTML document customisations from a React component.
62
+ * Automatically detects whether it is running on the server or the client.
63
+ */
64
+ export declare function useHtml(options: HtmlOptions): void;
@@ -0,0 +1,125 @@
1
+ import { useEffect } from "react";
2
+ import { getHtmlStore } from "./html-store.js";
3
+ function useHtml(options) {
4
+ if (typeof document === "undefined") {
5
+ serverUseHtml(options);
6
+ } else {
7
+ clientUseHtml(options);
8
+ }
9
+ }
10
+ function serverUseHtml(options) {
11
+ const store = getHtmlStore();
12
+ if (!store) return;
13
+ if (options.title !== void 0) store.titleOps.push(options.title);
14
+ if (options.htmlAttrs) Object.assign(store.htmlAttrs, options.htmlAttrs);
15
+ if (options.bodyAttrs) Object.assign(store.bodyAttrs, options.bodyAttrs);
16
+ if (options.meta?.length) store.meta.push(...options.meta);
17
+ if (options.link?.length) store.link.push(...options.link);
18
+ if (options.script?.length) store.script.push(...options.script);
19
+ if (options.style?.length) store.style.push(...options.style);
20
+ }
21
+ let _uid = 0;
22
+ const uid = () => `uh${++_uid}`;
23
+ function clientUseHtml(options) {
24
+ useEffect(() => {
25
+ if (options.title === void 0) return;
26
+ const prev = document.title;
27
+ document.title = typeof options.title === "function" ? options.title(prev) : options.title;
28
+ return () => {
29
+ document.title = prev;
30
+ };
31
+ }, [typeof options.title === "function" ? options.title.toString() : options.title]);
32
+ useEffect(() => {
33
+ if (!options.htmlAttrs) return;
34
+ return applyAttrs(document.documentElement, options.htmlAttrs);
35
+ }, [JSON.stringify(options.htmlAttrs)]);
36
+ useEffect(() => {
37
+ if (!options.bodyAttrs) return;
38
+ return applyAttrs(document.body, options.bodyAttrs);
39
+ }, [JSON.stringify(options.bodyAttrs)]);
40
+ useEffect(() => {
41
+ if (!options.meta?.length) return;
42
+ const id = uid();
43
+ const nodes = options.meta.map((tag) => {
44
+ const el = document.createElement("meta");
45
+ for (const [k, v] of Object.entries(tag)) {
46
+ if (v !== void 0) el.setAttribute(domAttr(k), v);
47
+ }
48
+ el.dataset.usehtml = id;
49
+ document.head.appendChild(el);
50
+ return el;
51
+ });
52
+ return () => nodes.forEach((n) => n.remove());
53
+ }, [JSON.stringify(options.meta)]);
54
+ useEffect(() => {
55
+ if (!options.link?.length) return;
56
+ const id = uid();
57
+ const nodes = options.link.map((tag) => {
58
+ const el = document.createElement("link");
59
+ for (const [k, v] of Object.entries(tag)) {
60
+ if (v !== void 0) el.setAttribute(domAttr(k), v);
61
+ }
62
+ el.dataset.usehtml = id;
63
+ document.head.appendChild(el);
64
+ return el;
65
+ });
66
+ return () => nodes.forEach((n) => n.remove());
67
+ }, [JSON.stringify(options.link)]);
68
+ useEffect(() => {
69
+ if (!options.script?.length) return;
70
+ const id = uid();
71
+ const nodes = options.script.map((tag) => {
72
+ const el = document.createElement("script");
73
+ if (tag.src) el.src = tag.src;
74
+ if (tag.type) el.type = tag.type;
75
+ if (tag.defer) el.defer = true;
76
+ if (tag.async) el.async = true;
77
+ if (tag.noModule) el.setAttribute("nomodule", "");
78
+ if (tag.crossOrigin) el.crossOrigin = tag.crossOrigin;
79
+ if (tag.integrity) el.integrity = tag.integrity;
80
+ if (tag.content) el.textContent = tag.content;
81
+ el.dataset.usehtml = id;
82
+ document.head.appendChild(el);
83
+ return el;
84
+ });
85
+ return () => nodes.forEach((n) => n.remove());
86
+ }, [JSON.stringify(options.script)]);
87
+ useEffect(() => {
88
+ if (!options.style?.length) return;
89
+ const id = uid();
90
+ const nodes = options.style.map((tag) => {
91
+ const el = document.createElement("style");
92
+ if (tag.media) el.media = tag.media;
93
+ if (tag.content) el.textContent = tag.content;
94
+ el.dataset.usehtml = id;
95
+ document.head.appendChild(el);
96
+ return el;
97
+ });
98
+ return () => nodes.forEach((n) => n.remove());
99
+ }, [JSON.stringify(options.style)]);
100
+ }
101
+ function applyAttrs(el, attrs) {
102
+ const prev = {};
103
+ for (const [k, v] of Object.entries(attrs)) {
104
+ if (v === void 0) continue;
105
+ const attr = domAttr(k);
106
+ prev[attr] = el.getAttribute(attr);
107
+ el.setAttribute(attr, v);
108
+ }
109
+ return () => {
110
+ for (const [attr, was] of Object.entries(prev)) {
111
+ if (was === null) el.removeAttribute(attr);
112
+ else el.setAttribute(attr, was);
113
+ }
114
+ };
115
+ }
116
+ function domAttr(key) {
117
+ if (key === "httpEquiv") return "http-equiv";
118
+ if (key === "hrefLang") return "hreflang";
119
+ if (key === "crossOrigin") return "crossorigin";
120
+ return key;
121
+ }
122
+ export {
123
+ useHtml
124
+ };
125
+ //# sourceMappingURL=use-html.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }