nukejs 0.0.30 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/builder.js CHANGED
@@ -41,6 +41,7 @@ function processDist(dir) {
41
41
  }
42
42
  const PUBLIC_STEMS = /* @__PURE__ */ new Set([
43
43
  "index",
44
+ "server",
44
45
  "html-store",
45
46
  // exported types (TitleValue, LinkTag, MetaTag, …) live here
46
47
  "use-html",
package/dist/index.d.ts CHANGED
@@ -9,8 +9,6 @@ export type { RequestContext } from './use-request';
9
9
  export { normaliseHeaders, sanitiseHeaders, getRequestStore } from './request-store';
10
10
  export { cache } from './cache-store';
11
11
  export { default as Link } from './Link';
12
- export { renderComponent } from './render-component';
13
- export type { RenderComponentOptions } from './render-component';
14
12
  export { setupLocationChangeMonitor, initRuntime } from './bundle';
15
13
  export type { RuntimeData } from './bundle';
16
14
  export { escapeHtml } from './utils';
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ import { useRequest } from "./use-request.js";
5
5
  import { normaliseHeaders, sanitiseHeaders, getRequestStore } from "./request-store.js";
6
6
  import { cache } from "./cache-store.js";
7
7
  import { default as default3 } from "./Link.js";
8
- import { renderComponent } from "./render-component.js";
9
8
  import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
10
9
  import { escapeHtml } from "./utils.js";
11
10
  import { ansi, c, log, setDebugLevel, getDebugLevel } from "./logger.js";
@@ -22,7 +21,6 @@ export {
22
21
  initRuntime,
23
22
  log,
24
23
  normaliseHeaders,
25
- renderComponent,
26
24
  sanitiseHeaders,
27
25
  setDebugLevel,
28
26
  setupLocationChangeMonitor,
@@ -1,28 +1,19 @@
1
1
  import { createElement } from "react";
2
- import { pathToFileURL } from "url";
3
- import { tsImport } from "tsx/esm/api";
4
- import path from "path";
5
- import { renderDocument } from "./ssr.js";
6
- async function renderComponent(filePath, props = {}, options = {}) {
7
- const layoutPaths = options.layoutPaths ?? [];
8
- const { default: Component } = await tsImport(pathToFileURL(filePath).href, { parentURL: import.meta.url });
2
+ import { renderDocument } from "./render-document.js";
3
+ async function renderComponent(Component, props = {}, options = {}) {
4
+ const layouts = options.layouts ?? [];
9
5
  let element = createElement(Component, props);
10
- for (let i = layoutPaths.length - 1; i >= 0; i--) {
11
- const { default: Layout } = await tsImport(pathToFileURL(layoutPaths[i]).href, { parentURL: import.meta.url });
12
- element = createElement(Layout, { children: element });
6
+ for (let i = layouts.length - 1; i >= 0; i--) {
7
+ element = createElement(layouts[i], { children: element });
13
8
  }
14
9
  return renderDocument({
15
- registryEntryFiles: [filePath, ...layoutPaths],
16
- pagesDir: options.pagesDir ?? path.resolve("./app/pages"),
17
10
  element,
11
+ // No clientRegistry / resolveComponentCache — pure SSR, see module doc.
18
12
  url: options.url ?? "/",
19
13
  params: options.params,
20
14
  query: options.query,
21
15
  headers: options.headers,
22
16
  isDev: options.isDev ?? process.env.ENVIRONMENT !== "production",
23
- // Always false here: the whole point is a real server-rendered
24
- // document for crawlers, not just a hydration target. skipClientSSR
25
- // exists in renderDocument only for ssr.ts's dev-mode HMR fast path.
26
17
  skipClientSSR: false,
27
18
  defaultTitle: options.title ?? "NukeJS"
28
19
  });
@@ -0,0 +1,165 @@
1
+ import { renderElementToHtml } from "./renderer.js";
2
+ import { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from "./request-store.js";
3
+ import { runWithCacheStore } from "./cache-store.js";
4
+ import {
5
+ runWithHtmlStore,
6
+ resolveTitle
7
+ } from "./html-store.js";
8
+ import { getDebugLevel } from "./logger.js";
9
+ function toClientDebugLevel(level) {
10
+ if (level === true) return "verbose";
11
+ if (level === "info") return "info";
12
+ if (level === "error") return "error";
13
+ return "silent";
14
+ }
15
+ function escapeAttr(str) {
16
+ return str.replace(/&/g, "&").replace(/"/g, """);
17
+ }
18
+ function renderAttrs(attrs) {
19
+ return Object.entries(attrs).filter(([, v]) => v !== void 0 && v !== false).map(([k, v]) => v === true ? k : `${k}="${escapeAttr(String(v))}"`).join(" ");
20
+ }
21
+ function openTag(tag, attrs) {
22
+ const str = renderAttrs(attrs);
23
+ return str ? `<${tag} ${str}>` : `<${tag}>`;
24
+ }
25
+ function metaKey(k) {
26
+ return k === "httpEquiv" ? "http-equiv" : k;
27
+ }
28
+ function linkKey(k) {
29
+ if (k === "hrefLang") return "hreflang";
30
+ if (k === "crossOrigin") return "crossorigin";
31
+ return k;
32
+ }
33
+ function renderMetaTag(tag) {
34
+ const attrs = {};
35
+ for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[metaKey(k)] = v;
36
+ return ` <meta ${renderAttrs(attrs)} />`;
37
+ }
38
+ function renderLinkTag(tag) {
39
+ const attrs = {};
40
+ for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[linkKey(k)] = v;
41
+ return ` <link ${renderAttrs(attrs)} />`;
42
+ }
43
+ function renderScriptTag(tag) {
44
+ const attrs = {
45
+ src: tag.src,
46
+ type: tag.type,
47
+ crossorigin: tag.crossOrigin,
48
+ integrity: tag.integrity,
49
+ defer: tag.defer,
50
+ async: tag.async,
51
+ nomodule: tag.noModule
52
+ };
53
+ const attrStr = renderAttrs(attrs);
54
+ const open = attrStr ? `<script ${attrStr}>` : "<script>";
55
+ return ` ${open}${tag.src ? "" : tag.content ?? ""}</script>`;
56
+ }
57
+ function renderStyleTag(tag) {
58
+ const media = tag.media ? ` media="${escapeAttr(tag.media)}"` : "";
59
+ return ` <style${media}>${tag.content ?? ""}</style>`;
60
+ }
61
+ function renderManagedHeadTags(store) {
62
+ const headScripts = store.script.filter((s) => (s.position ?? "head") === "head");
63
+ const tags = [
64
+ ...store.meta.map(renderMetaTag),
65
+ ...store.link.map(renderLinkTag),
66
+ ...store.style.map(renderStyleTag),
67
+ ...headScripts.map(renderScriptTag)
68
+ ];
69
+ if (tags.length === 0) return [];
70
+ return [" <!--n-head-->", ...tags, " <!--/n-head-->"];
71
+ }
72
+ function renderManagedBodyScripts(store) {
73
+ const bodyScripts = store.script.filter((s) => s.position === "body");
74
+ if (bodyScripts.length === 0) return [];
75
+ return [" <!--n-body-scripts-->", ...bodyScripts.map(renderScriptTag), " <!--/n-body-scripts-->"];
76
+ }
77
+ async function renderDocument(options) {
78
+ const {
79
+ element,
80
+ clientRegistry,
81
+ resolveComponentCache,
82
+ url,
83
+ params = {},
84
+ query = {},
85
+ headers = {},
86
+ isDev = false,
87
+ skipClientSSR = false,
88
+ defaultTitle = "NukeJS"
89
+ } = options;
90
+ const cleanUrl = url.split("?")[0];
91
+ const normHeaders = normaliseHeaders(headers);
92
+ const safeHeaders = sanitiseHeaders(headers);
93
+ const registry = clientRegistry ?? /* @__PURE__ */ new Map();
94
+ const ctx = {
95
+ registry,
96
+ hydrated: /* @__PURE__ */ new Set(),
97
+ skipClientSSR,
98
+ getComponentCache: resolveComponentCache
99
+ };
100
+ let appHtml = "";
101
+ const store = await runWithRequestStore(
102
+ {
103
+ url,
104
+ pathname: cleanUrl,
105
+ params,
106
+ query,
107
+ headers: normHeaders
108
+ },
109
+ () => runWithCacheStore(() => runWithHtmlStore(async () => {
110
+ appHtml = await renderElementToHtml(element, ctx);
111
+ }))
112
+ );
113
+ const pageTitle = resolveTitle(store.titleOps, defaultTitle);
114
+ const headLines = [
115
+ ' <meta charset="utf-8" />',
116
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
117
+ ` <title>${escapeAttr(pageTitle)}</title>`,
118
+ ...renderManagedHeadTags(store)
119
+ ];
120
+ const runtimeData = JSON.stringify({
121
+ hydrateIds: [...ctx.hydrated],
122
+ allIds: [...registry.keys()],
123
+ url,
124
+ params,
125
+ query,
126
+ headers: safeHeaders,
127
+ debug: toClientDebugLevel(getDebugLevel())
128
+ }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
129
+ const bodyScriptLines = renderManagedBodyScripts(store);
130
+ const bodyScriptsHtml = bodyScriptLines.length > 0 ? "\n" + bodyScriptLines.join("\n") + "\n" : "";
131
+ return `<!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
+ ${bodyScriptsHtml}</body>
161
+ </html>`;
162
+ }
163
+ export {
164
+ renderDocument
165
+ };
package/dist/renderer.js CHANGED
@@ -1,8 +1,6 @@
1
- import path from "path";
2
1
  import { createElement, Fragment } from "react";
3
2
  import { renderToString } from "react-dom/server";
4
3
  import { log } from "./logger.js";
5
- import { getComponentCache } from "./component-analyzer.js";
6
4
  import { escapeHtml } from "./utils.js";
7
5
  function isWrapperAttr(key) {
8
6
  return key === "className" || key === "style" || key === "id" || key.startsWith("data-") || key.startsWith("aria-");
@@ -77,7 +75,7 @@ async function renderHtmlElement(type, props, ctx) {
77
75
  return `<${type}${attrStr}>${childrenHtml}</${type}>`;
78
76
  }
79
77
  async function renderFunctionComponent(type, props, ctx) {
80
- const componentCache = getComponentCache();
78
+ const componentCache = ctx.getComponentCache ? ctx.getComponentCache() : /* @__PURE__ */ new Map();
81
79
  for (const [id, filePath] of ctx.registry.entries()) {
82
80
  const info = componentCache.get(filePath);
83
81
  if (!info?.isClientComponent) continue;
@@ -91,7 +89,7 @@ async function renderFunctionComponent(type, props, ctx) {
91
89
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
92
90
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
93
91
  const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps, ctx);
94
- log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
92
+ log.verbose(`Client component rendered for hydration: ${id} (${filePath.split(/[\\/]/).pop()})`);
95
93
  const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, hydrationSafeProps));
96
94
  return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
97
95
  JSON.stringify(serializedProps)
@@ -148,7 +146,7 @@ async function prepareElement(element, ctx) {
148
146
  return { real: createElement(type, p.real), json: { __re: "html", tag: type, props: p.json } };
149
147
  }
150
148
  if (typeof type === "function") {
151
- const componentCache = getComponentCache();
149
+ const componentCache = ctx.getComponentCache ? ctx.getComponentCache() : /* @__PURE__ */ new Map();
152
150
  for (const [id, filePath] of ctx.registry.entries()) {
153
151
  const info = componentCache.get(filePath);
154
152
  if (!info?.isClientComponent) continue;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * server.ts — server-only public API.
3
+ *
4
+ * Kept separate from index.ts on purpose: index.ts is the entry point the
5
+ * client component bundler (bundler.ts, platform: 'browser') resolves
6
+ * whenever a "use client" file imports from 'nukejs'. renderComponent()
7
+ * pulls in tsx/esm/api and, transitively, ssr.ts's Node built-ins (fs,
8
+ * node:worker_threads, …) — none of which esbuild can (or should) resolve
9
+ * for a browser bundle. Anything server-only belongs here, under
10
+ * 'nukejs/server', not in the shared index.
11
+ */
12
+ export { renderComponent } from './render-component';
13
+ export type { RenderComponentOptions } from './render-component';
package/dist/server.js ADDED
@@ -0,0 +1,4 @@
1
+ import { renderComponent } from "./render-component.js";
2
+ export {
3
+ renderComponent
4
+ };
package/dist/ssr.js CHANGED
@@ -3,16 +3,10 @@ import fs from "fs";
3
3
  import { createElement } from "react";
4
4
  import { pathToFileURL } from "url";
5
5
  import { tsImport } from "tsx/esm/api";
6
- import { log, getDebugLevel } from "./logger.js";
6
+ import { log } from "./logger.js";
7
7
  import { matchRoute, findLayoutsForRoute } from "./router.js";
8
- import { findClientComponentsInTree } from "./component-analyzer.js";
9
- import { renderElementToHtml } from "./renderer.js";
10
- import { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from "./request-store.js";
11
- import { runWithCacheStore } from "./cache-store.js";
12
- import {
13
- runWithHtmlStore,
14
- resolveTitle
15
- } from "./html-store.js";
8
+ import { findClientComponentsInTree, getComponentCache } from "./component-analyzer.js";
9
+ import { renderDocument } from "./render-document.js";
16
10
  async function wrapWithLayouts(pageElement, layoutPaths) {
17
11
  let element = pageElement;
18
12
  for (let i = layoutPaths.length - 1; i >= 0; i--) {
@@ -24,158 +18,6 @@ async function wrapWithLayouts(pageElement, layoutPaths) {
24
18
  }
25
19
  return element;
26
20
  }
27
- function toClientDebugLevel(level) {
28
- if (level === true) return "verbose";
29
- if (level === "info") return "info";
30
- if (level === "error") return "error";
31
- return "silent";
32
- }
33
- function escapeAttr(str) {
34
- return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
35
- }
36
- function renderAttrs(attrs) {
37
- return Object.entries(attrs).filter(([, v]) => v !== void 0 && v !== false).map(([k, v]) => v === true ? k : `${k}="${escapeAttr(String(v))}"`).join(" ");
38
- }
39
- function openTag(tag, attrs) {
40
- const str = renderAttrs(attrs);
41
- return str ? `<${tag} ${str}>` : `<${tag}>`;
42
- }
43
- function metaKey(k) {
44
- return k === "httpEquiv" ? "http-equiv" : k;
45
- }
46
- function linkKey(k) {
47
- if (k === "hrefLang") return "hreflang";
48
- if (k === "crossOrigin") return "crossorigin";
49
- return k;
50
- }
51
- function renderMetaTag(tag) {
52
- const attrs = {};
53
- for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[metaKey(k)] = v;
54
- return ` <meta ${renderAttrs(attrs)} />`;
55
- }
56
- function renderLinkTag(tag) {
57
- const attrs = {};
58
- for (const [k, v] of Object.entries(tag)) if (v !== void 0) attrs[linkKey(k)] = v;
59
- return ` <link ${renderAttrs(attrs)} />`;
60
- }
61
- function renderScriptTag(tag) {
62
- const attrs = {
63
- src: tag.src,
64
- type: tag.type,
65
- crossorigin: tag.crossOrigin,
66
- integrity: tag.integrity,
67
- defer: tag.defer,
68
- async: tag.async,
69
- nomodule: tag.noModule
70
- };
71
- const attrStr = renderAttrs(attrs);
72
- const open = attrStr ? `<script ${attrStr}>` : "<script>";
73
- return ` ${open}${tag.src ? "" : tag.content ?? ""}</script>`;
74
- }
75
- function renderStyleTag(tag) {
76
- const media = tag.media ? ` media="${escapeAttr(tag.media)}"` : "";
77
- return ` <style${media}>${tag.content ?? ""}</style>`;
78
- }
79
- function renderManagedHeadTags(store) {
80
- const headScripts = store.script.filter((s) => (s.position ?? "head") === "head");
81
- const tags = [
82
- ...store.meta.map(renderMetaTag),
83
- ...store.link.map(renderLinkTag),
84
- ...store.style.map(renderStyleTag),
85
- ...headScripts.map(renderScriptTag)
86
- ];
87
- if (tags.length === 0) return [];
88
- return [" <!--n-head-->", ...tags, " <!--/n-head-->"];
89
- }
90
- function renderManagedBodyScripts(store) {
91
- const bodyScripts = store.script.filter((s) => s.position === "body");
92
- if (bodyScripts.length === 0) return [];
93
- return [" <!--n-body-scripts-->", ...bodyScripts.map(renderScriptTag), " <!--/n-body-scripts-->"];
94
- }
95
- async function renderDocument(options) {
96
- const {
97
- registryEntryFiles,
98
- pagesDir,
99
- element,
100
- url,
101
- params = {},
102
- query = {},
103
- headers = {},
104
- isDev = false,
105
- skipClientSSR = false,
106
- defaultTitle = "NukeJS"
107
- } = options;
108
- const cleanUrl = url.split("?")[0];
109
- const normHeaders = normaliseHeaders(headers);
110
- const safeHeaders = sanitiseHeaders(headers);
111
- const registry = /* @__PURE__ */ new Map();
112
- for (const entryFile of registryEntryFiles)
113
- for (const [id, p] of findClientComponentsInTree(entryFile, pagesDir))
114
- registry.set(id, p);
115
- const ctx = { registry, hydrated: /* @__PURE__ */ new Set(), skipClientSSR };
116
- let appHtml = "";
117
- const store = await runWithRequestStore(
118
- {
119
- url,
120
- pathname: cleanUrl,
121
- params,
122
- query,
123
- headers: normHeaders
124
- },
125
- () => runWithCacheStore(() => runWithHtmlStore(async () => {
126
- appHtml = await renderElementToHtml(element, ctx);
127
- }))
128
- );
129
- const pageTitle = resolveTitle(store.titleOps, defaultTitle);
130
- const headLines = [
131
- ' <meta charset="utf-8" />',
132
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
133
- ` <title>${escapeAttr(pageTitle)}</title>`,
134
- ...renderManagedHeadTags(store)
135
- ];
136
- const runtimeData = JSON.stringify({
137
- hydrateIds: [...ctx.hydrated],
138
- allIds: [...registry.keys()],
139
- url,
140
- params,
141
- query,
142
- headers: safeHeaders,
143
- debug: toClientDebugLevel(getDebugLevel())
144
- }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
145
- const bodyScriptLines = renderManagedBodyScripts(store);
146
- const bodyScriptsHtml = bodyScriptLines.length > 0 ? "\n" + bodyScriptLines.join("\n") + "\n" : "";
147
- return `<!DOCTYPE html>
148
- ${openTag("html", store.htmlAttrs)}
149
- <head>
150
- ${headLines.join("\n")}
151
- </head>
152
- ${openTag("body", store.bodyAttrs)}
153
- <div id="app">${appHtml}</div>
154
-
155
- <script id="__n_data" type="application/json">${runtimeData}</script>
156
-
157
- <script type="importmap">
158
- {
159
- "imports": {
160
- "react": "/__react.js",
161
- "react-dom/client": "/__react.js",
162
- "react/jsx-runtime": "/__react.js",
163
- "nukejs": "/__n.js"
164
- }
165
- }
166
- </script>
167
-
168
- <script type="module">
169
- await import('react');
170
- const { initRuntime } = await import('nukejs');
171
- const data = JSON.parse(document.getElementById('__n_data').textContent);
172
- initRuntime(data);
173
- </script>
174
-
175
- ${isDev ? '<script type="module" src="/__hmr.js"></script>' : ""}
176
- ${bodyScriptsHtml}</body>
177
- </html>`;
178
- }
179
21
  async function renderFile(filePath, params, url, pagesDir, isDev, res, req, statusCode, skipClientSSR) {
180
22
  const searchParams = new URL(url, "http://localhost").searchParams;
181
23
  const queryParams = {};
@@ -195,10 +37,14 @@ async function renderFile(filePath, params, url, pagesDir, isDev, res, req, stat
195
37
  createElement(PageComponent, mergedParams),
196
38
  layoutPaths
197
39
  );
40
+ const clientRegistry = /* @__PURE__ */ new Map();
41
+ for (const entryFile of [filePath, ...layoutPaths])
42
+ for (const [id, p] of findClientComponentsInTree(entryFile, pagesDir))
43
+ clientRegistry.set(id, p);
198
44
  const html = await renderDocument({
199
- registryEntryFiles: [filePath, ...layoutPaths],
200
- pagesDir,
201
45
  element: wrappedElement,
46
+ clientRegistry,
47
+ resolveComponentCache: getComponentCache,
202
48
  url,
203
49
  params,
204
50
  query: queryParams,
@@ -282,6 +128,5 @@ async function serverSideRender(url, res, pagesDir, isDev = false, req) {
282
128
  }
283
129
  }
284
130
  export {
285
- renderDocument,
286
131
  serverSideRender
287
132
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
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",
@@ -8,6 +8,10 @@
8
8
  ".": {
9
9
  "import": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts"
11
+ },
12
+ "./server": {
13
+ "import": "./dist/server.js",
14
+ "types": "./dist/server.d.ts"
11
15
  }
12
16
  },
13
17
  "bin": {