dinou 1.4.3 → 1.6.0

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/README.md CHANGED
@@ -54,6 +54,8 @@ dinou main features are:
54
54
 
55
55
  - Support for the use of an import alias in `tsconfig.json` or `jsconfig.json` file.
56
56
 
57
+ - Error handling with `error.tsx` pages, differentiationg behaviour in production and in development.
58
+
57
59
  ## Table of contents
58
60
 
59
61
  - [Routing system, layouts, pages, not found pages, ...](#routing-system-layouts-pages-not-found-pages-)
@@ -98,6 +100,8 @@ dinou main features are:
98
100
 
99
101
  - [Not Found Handling](#not-found-handling)
100
102
 
103
+ - [Error Handling](#error-handling)
104
+
101
105
  - [`favicons` folder](#favicons-folder)
102
106
 
103
107
  - [`.env` file](#env-file)
@@ -126,6 +130,10 @@ dinou main features are:
126
130
 
127
131
  - If you don't want a `page` to be applied layouts define a `no_layout` file (without extension) in the same folder. A `no_layout` file, if present, also applies to the `not_found` file if present in the same folder. There exists also a `no_layout_not_found` file if you don't want a `not_found` file to be applied layouts but you do in `page` component.
128
132
 
133
+ - `reset_layout` file (without extension) if present in the same folder as a `layout.tsx` file, will ignore previous layouts in the layout hierarchy.
134
+
135
+ - If found any `error.tsx` (or `.jsx`) page in the route hierarchy, the more nested one will be rendered in case of error in the page. Layouts are also applied to error pages if no `no_layout` or `no_layout_error` files (without extension) exists in the folder where `error.tsx` is defined.
136
+
129
137
  ## page_functions.ts (or `.tsx`, `.js`, `.jsx`)
130
138
 
131
139
  `page_functions.ts` is a file for defining four diferent possible functions. These are:
@@ -841,6 +849,8 @@ The routing system is file-based and supports static routes, dynamic routes, opt
841
849
 
842
850
  - If a **`no_layout`** file exists in a directory (**without extension**), the layout hierarchy is skipped, and only the page content is rendered.
843
851
 
852
+ - If a **`reset_layout`** file (**without extension**) exists in a directory where a `layout.tsx` file is defined, previous layouts in the hierarchy will be ignored.
853
+
844
854
  ### Not Found Handling
845
855
 
846
856
  - If no `page.tsx` is found for a route, the system looks for a `not_found.tsx` file in the route hierarchy.
@@ -855,6 +865,51 @@ The routing system is file-based and supports static routes, dynamic routes, opt
855
865
 
856
866
  - Layouts are applied to `not_found.tsx` pages too, unless a `no_layout` or **`no_layout_not_found`** files (**without extension**) are found in the directory in which the `not_found.tsx` page is defined, in which case layouts will not be applied to `not_found.tsx` page.
857
867
 
868
+ ### Error Handling
869
+
870
+ - In case of error in a page, the more nested `error.tsx` (or `.jsx`) page will rendered if exists. **If it does not exist, then in production the error will be written in the console, and in development a default error page will be rendered informing about the error message and the error stack**.
871
+
872
+ - Layouts are applied to `error.tsx` pages, if no `no_layout` or `no_layout_error` files (without extension) exists in the folder where `error.tsx` is defined.
873
+
874
+ - `error.tsx` pages are **dynamically rendered**, so avoid using server components (async functions) and fetching data in their body definition because this will delay the rendering of the page. Use `Suspense` instead if you need to fetch data.
875
+
876
+ - There not exists a `error_functions.ts` functionality, so there is no `getProps` for error pages. Again, if you need to fetch data use `Suspense`.
877
+
878
+ - The error page receives `params`, `query`, and `error`. `error` is an object with properties `message` and `stack` which are strings.
879
+
880
+ - Example:
881
+
882
+ ```typescript
883
+ "use client";
884
+
885
+ export default function Page({
886
+ error: { message, stack },
887
+ }: {
888
+ error: Error;
889
+ }) {
890
+ return (
891
+ <main className="flex-1 flex flex-col items-center justify-center p-4">
892
+ <div className="max-w-md w-full text-center space-y-6">
893
+ <h1 className="text-3xl font-bold text-red-600">Error</h1>
894
+ <p className="text-lg text-gray-700">
895
+ An unexpected error has occurred baby. Please try again later.
896
+ </p>
897
+ <a
898
+ href="/"
899
+ className="inline-block px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
900
+ >
901
+ Go to Home
902
+ </a>
903
+ </div>
904
+ <div className="mt-6 text-sm text-gray-500">
905
+ <pre className="whitespace-pre-wrap break-words">{message}</pre>
906
+ <pre className="whitespace-pre-wrap break-words">{stack}</pre>
907
+ </div>
908
+ </main>
909
+ );
910
+ }
911
+ ```
912
+
858
913
  ## `favicons` folder
859
914
 
860
915
  If you want to show a favicon, generate one with an online tool (e.g. [favicon.io](https://favicon.io/)), unzip the downloaded folder with the favicons, paste it in the root of the project and rename it to `favicons`. Then update your `layout` or `page` to include this in the `head` tag:
@@ -382,6 +382,18 @@ async function buildStaticPages() {
382
382
  }
383
383
  jsx = React.createElement(Layout, props, jsx);
384
384
  jsx = { ...jsx, __modulePath: layoutPath };
385
+ const layoutFolderPath = path.dirname(layoutPath);
386
+ if (
387
+ getFilePathAndDynamicParams(
388
+ [],
389
+ {},
390
+ layoutFolderPath,
391
+ "reset_layout",
392
+ false
393
+ )[0]
394
+ ) {
395
+ break;
396
+ }
385
397
  index++;
386
398
  }
387
399
  }
@@ -548,6 +560,18 @@ async function buildStaticPage(reqPath) {
548
560
  }
549
561
  jsx = React.createElement(Layout, layoutProps, jsx);
550
562
  jsx = { ...jsx, __modulePath: layoutPath };
563
+ const layoutFolderPath = path.dirname(layoutPath);
564
+ if (
565
+ getFilePathAndDynamicParams(
566
+ [],
567
+ {},
568
+ layoutFolderPath,
569
+ "reset_layout",
570
+ false
571
+ )[0]
572
+ ) {
573
+ break;
574
+ }
551
575
  index++;
552
576
  }
553
577
  }
@@ -0,0 +1,36 @@
1
+ import { use } from "react";
2
+ import { createFromFetch } from "react-server-dom-webpack/client";
3
+ import { hydrateRoot } from "react-dom/client";
4
+
5
+ const cache = new Map();
6
+ const route = window.location.href.replace(window.location.origin, "");
7
+
8
+ function Root() {
9
+ let content = cache.get(route);
10
+ if (!content) {
11
+ content = createFromFetch(
12
+ fetch("/____rsc_payload_error____" + route, {
13
+ method: "POST",
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ },
17
+ body: JSON.stringify({
18
+ error: {
19
+ message: window.__DINOU_ERROR_MESSAGE__ || "Unknown error",
20
+ stack: window.__DINOU_ERROR_STACK__ || "No stack trace available",
21
+ },
22
+ }),
23
+ })
24
+ );
25
+ cache.set(route, content);
26
+ }
27
+
28
+ return use(content);
29
+ }
30
+
31
+ hydrateRoot(document, <Root />);
32
+
33
+ // HMR
34
+ if (import.meta.hot) {
35
+ import.meta.hot.accept();
36
+ }
@@ -0,0 +1,122 @@
1
+ const path = require("path");
2
+ const { existsSync } = require("fs");
3
+ const React = require("react");
4
+ const {
5
+ getFilePathAndDynamicParams,
6
+ } = require("./get-file-path-and-dynamic-params");
7
+
8
+ function getErrorJSX(reqPath, query, error) {
9
+ const srcFolder = path.resolve(process.cwd(), "src");
10
+ const reqSegments = reqPath.split("/").filter(Boolean);
11
+ const folderPath = path.join(srcFolder, ...reqSegments);
12
+ let pagePath;
13
+ if (existsSync(folderPath)) {
14
+ for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
15
+ const candidatePath = path.join(folderPath, `error${ext}`);
16
+ if (existsSync(candidatePath)) {
17
+ pagePath = candidatePath;
18
+ }
19
+ }
20
+ }
21
+ let dynamicParams;
22
+
23
+ if (!pagePath) {
24
+ const [filePath, dParams] = getFilePathAndDynamicParams(
25
+ reqSegments,
26
+ query,
27
+ srcFolder,
28
+ "error"
29
+ );
30
+ pagePath = filePath;
31
+ dynamicParams = dParams ?? {};
32
+ }
33
+
34
+ let jsx;
35
+
36
+ if (!pagePath) {
37
+ const [errorPath, dParams] = getFilePathAndDynamicParams(
38
+ reqSegments,
39
+ query,
40
+ srcFolder,
41
+ "error",
42
+ true,
43
+ false
44
+ );
45
+ if (errorPath) {
46
+ pagePath = errorPath;
47
+ dynamicParams = dParams ?? {};
48
+ }
49
+ }
50
+
51
+ if (pagePath) {
52
+ const pageModule = require(pagePath);
53
+ const Page = pageModule.default ?? pageModule;
54
+ jsx = React.createElement(Page, {
55
+ params: dynamicParams ?? {},
56
+ query,
57
+ error,
58
+ });
59
+
60
+ const noLayoutErrorPath = path.join(
61
+ pagePath.split("\\").slice(0, -1).join("\\"),
62
+ `no_layout_error`
63
+ );
64
+ if (existsSync(path.resolve(process.cwd(), `${noLayoutErrorPath}`))) {
65
+ return jsx;
66
+ }
67
+
68
+ if (
69
+ getFilePathAndDynamicParams(
70
+ reqSegments,
71
+ query,
72
+ srcFolder,
73
+ "no_layout",
74
+ false
75
+ )[0]
76
+ ) {
77
+ return jsx;
78
+ }
79
+
80
+ const layouts = getFilePathAndDynamicParams(
81
+ reqSegments,
82
+ query,
83
+ srcFolder,
84
+ "layout",
85
+ true,
86
+ false,
87
+ undefined,
88
+ 0,
89
+ {},
90
+ true
91
+ );
92
+
93
+ if (layouts && Array.isArray(layouts)) {
94
+ let index = 0;
95
+ for (const [layoutPath, dParams, slots] of layouts.reverse()) {
96
+ const layoutModule = require(layoutPath);
97
+ const Layout = layoutModule.default ?? layoutModule;
98
+ let props = { params: dParams, query, ...slots };
99
+ jsx = React.createElement(Layout, props, jsx);
100
+ const layoutFolderPath = path.dirname(layoutPath);
101
+ if (
102
+ getFilePathAndDynamicParams(
103
+ [],
104
+ {},
105
+ layoutFolderPath,
106
+ "reset_layout",
107
+ false
108
+ )[0]
109
+ ) {
110
+ break;
111
+ }
112
+ index++;
113
+ }
114
+ }
115
+ }
116
+
117
+ return jsx;
118
+ }
119
+
120
+ module.exports = {
121
+ getErrorJSX,
122
+ };
package/dinou/get-jsx.js CHANGED
@@ -129,6 +129,18 @@ async function getJSX(reqPath, query) {
129
129
  props = { ...props, ...(pageFunctionsProps?.layout ?? {}) };
130
130
  }
131
131
  jsx = React.createElement(Layout, props, jsx);
132
+ const layoutFolderPath = path.dirname(layoutPath);
133
+ if (
134
+ getFilePathAndDynamicParams(
135
+ [],
136
+ {},
137
+ layoutFolderPath,
138
+ "reset_layout",
139
+ false
140
+ )[0]
141
+ ) {
142
+ break;
143
+ }
132
144
  index++;
133
145
  }
134
146
  }
@@ -25,14 +25,92 @@ addHook({
25
25
 
26
26
  const { renderToPipeableStream } = require("react-dom/server");
27
27
  const { getJSX, getSSGJSX } = require("./get-jsx");
28
+ const { getErrorJSX } = require("./get-error-jsx");
28
29
  const { renderJSXToClientJSX } = require("./render-jsx-to-client-jsx");
29
30
 
30
- // Render the app to a stream
31
- async function renderToStream() {
32
- try {
33
- const reqPath = process.argv[2];
34
- const query = JSON.parse(process.argv[3]);
31
+ function formatErrorHtml(error) {
32
+ const message = error.message || "Unknown error";
33
+ const stack = error.stack
34
+ ? error.stack.replace(/\n/g, "<br>").replace(/\s/g, "&nbsp;")
35
+ : "No stack trace available";
36
+
37
+ return `
38
+ <!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
+ <title>Error</title>
44
+ <style>
45
+ body {
46
+ font-family: Arial, sans-serif;
47
+ margin: 0;
48
+ padding: 20px;
49
+ background-color: #f8f8f8;
50
+ color: #333;
51
+ }
52
+ .error-container {
53
+ max-width: 800px;
54
+ margin: 0 auto;
55
+ background-color: #fff;
56
+ padding: 20px;
57
+ border-radius: 8px;
58
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
59
+ }
60
+ .error-title {
61
+ color: #d32f2f;
62
+ font-size: 24px;
63
+ margin-bottom: 10px;
64
+ }
65
+ .error-message {
66
+ font-size: 18px;
67
+ margin-bottom: 20px;
68
+ }
69
+ .error-stack {
70
+ background-color: #f5f5f5;
71
+ padding: 15px;
72
+ border-radius: 4px;
73
+ font-family: Consolas, monospace;
74
+ font-size: 14px;
75
+ overflow-x: auto;
76
+ }
77
+ .error-footer {
78
+ margin-top: 20px;
79
+ font-size: 14px;
80
+ color: #666;
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+ <div class="error-container">
86
+ <h1 class="error-title">An Error Occurred</h1>
87
+ <p class="error-message">${message}</p>
88
+ <div class="error-stack">${stack}</div>
89
+ </div>
90
+ </body>
91
+ </html>
92
+ `;
93
+ }
94
+
95
+ function formatErrorHtmlProduction(error) {
96
+ const escapedMessage = JSON.stringify(`Render error: ${error.message}`);
97
+ const escapedStack = JSON.stringify(error.stack || "");
35
98
 
99
+ return `
100
+ <!DOCTYPE html>
101
+ <html>
102
+ <head><meta charset="utf-8"></head>
103
+ <body>
104
+ <script>
105
+ console.error(${escapedMessage} + "\\n" + ${escapedStack});
106
+ </script>
107
+ </body>
108
+ </html>
109
+ `;
110
+ }
111
+
112
+ async function renderToStream(reqPath, query) {
113
+ try {
36
114
  const jsx = Object.keys(query).length
37
115
  ? renderJSXToClientJSX(await getJSX(reqPath, query))
38
116
  : getSSGJSX(reqPath) ??
@@ -40,9 +118,54 @@ async function renderToStream() {
40
118
 
41
119
  const stream = renderToPipeableStream(jsx, {
42
120
  onError(error) {
43
- console.error("Render error:", error);
44
- process.stderr.write(JSON.stringify({ error: error.message }));
45
- process.exit(1);
121
+ const isProd = process.env.NODE_ENV === "production";
122
+
123
+ try {
124
+ const errorJSX = getErrorJSX(reqPath, query, error);
125
+
126
+ if (errorJSX === undefined) {
127
+ process.stdout.write(
128
+ isProd ? formatErrorHtmlProduction(error) : formatErrorHtml(error)
129
+ );
130
+ process.stderr.write(
131
+ JSON.stringify({ error: error.message, stack: error.stack })
132
+ );
133
+ process.exit(1);
134
+ }
135
+
136
+ const errorStream = renderToPipeableStream(errorJSX, {
137
+ onShellReady() {
138
+ errorStream.pipe(process.stdout);
139
+ },
140
+ onError(err) {
141
+ console.error("Error rendering error JSX:", err);
142
+ process.stdout.write(
143
+ isProd
144
+ ? formatErrorHtmlProduction(error)
145
+ : formatErrorHtml(error)
146
+ );
147
+ process.stderr.write(
148
+ JSON.stringify({ error: error.message, stack: error.stack })
149
+ );
150
+ process.exit(1);
151
+ },
152
+ bootstrapScripts: ["/error.js"],
153
+ bootstrapScriptContent: `window.__DINOU_ERROR_MESSAGE__=${JSON.stringify(
154
+ error.message || "Unknown error"
155
+ )};window.__DINOU_ERROR_STACK__=${JSON.stringify(
156
+ error.stack || "No stack trace available"
157
+ )};`,
158
+ });
159
+ } catch (err) {
160
+ console.error("Render error (no error.tsx?):", err);
161
+ process.stdout.write(
162
+ isProd ? formatErrorHtmlProduction(error) : formatErrorHtml(error)
163
+ );
164
+ process.stderr.write(
165
+ JSON.stringify({ error: error.message, stack: error.stack })
166
+ );
167
+ process.exit(1);
168
+ }
46
169
  },
47
170
  onShellReady() {
48
171
  stream.pipe(process.stdout);
@@ -50,21 +173,41 @@ async function renderToStream() {
50
173
  bootstrapScripts: ["/main.js"],
51
174
  });
52
175
  } catch (error) {
53
- process.stderr.write(JSON.stringify({ error: error.message }));
176
+ process.stdout.write(formatErrorHtml(error));
177
+ process.stderr.write(
178
+ JSON.stringify({
179
+ error: error.message,
180
+ stack: error.stack,
181
+ })
182
+ );
54
183
  process.exit(1);
55
184
  }
56
185
  }
57
186
 
187
+ const reqPath = process.argv[2];
188
+ const query = JSON.parse(process.argv[3]);
189
+
58
190
  process.on("uncaughtException", (error) => {
59
- process.stderr.write(JSON.stringify({ error: error.message }));
191
+ process.stdout.write(formatErrorHtml(error));
192
+ process.stderr.write(
193
+ JSON.stringify({
194
+ error: error.message,
195
+ stack: error.stack,
196
+ })
197
+ );
60
198
  process.exit(1);
61
199
  });
62
200
 
63
201
  process.on("unhandledRejection", (reason) => {
202
+ const error = reason instanceof Error ? reason : new Error(String(reason));
203
+ process.stdout.write(formatErrorHtml(error));
64
204
  process.stderr.write(
65
- JSON.stringify({ error: reason.message || "Unhandled promise rejection" })
205
+ JSON.stringify({
206
+ error: error.message,
207
+ stack: error.stack,
208
+ })
66
209
  );
67
210
  process.exit(1);
68
211
  });
69
212
 
70
- renderToStream();
213
+ renderToStream(reqPath, query);
package/dinou/server.js CHANGED
@@ -11,6 +11,7 @@ const webpackDevMiddleware = require("webpack-dev-middleware");
11
11
  const webpackHotMiddleware = require("webpack-hot-middleware");
12
12
  const webpackConfig = require(path.resolve(__dirname, "../webpack.config.js"));
13
13
  const { getSSGJSXOrJSX } = require("./get-jsx.js");
14
+ const { getErrorJSX } = require("./get-error-jsx.js");
14
15
  const addHook = require("./asset-require-hook.js");
15
16
  webpackRegister();
16
17
  const babelRegister = require("@babel/register");
@@ -37,6 +38,7 @@ addHook({
37
38
  });
38
39
 
39
40
  const app = express();
41
+ app.use(express.json());
40
42
  const isDevelopment = process.env.NODE_ENV !== "production";
41
43
 
42
44
  if (isDevelopment) {
@@ -57,7 +59,26 @@ app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
57
59
  const reqPath = (
58
60
  req.path.endsWith("/") ? req.path : req.path + "/"
59
61
  ).replace("/____rsc_payload____", "");
60
- jsx = await getSSGJSXOrJSX(reqPath, { ...req.query });
62
+ const jsx = await getSSGJSXOrJSX(reqPath, { ...req.query });
63
+ const manifest = readFileSync(
64
+ path.resolve(process.cwd(), "____public____/react-client-manifest.json"),
65
+ "utf8"
66
+ );
67
+ const moduleMap = JSON.parse(manifest);
68
+ const { pipe } = renderToPipeableStream(jsx, moduleMap);
69
+ pipe(res);
70
+ } catch (error) {
71
+ console.error("Error rendering RSC:", error);
72
+ res.status(500).send("Internal Server Error");
73
+ }
74
+ });
75
+
76
+ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
77
+ try {
78
+ const reqPath = (
79
+ req.path.endsWith("/") ? req.path : req.path + "/"
80
+ ).replace("/____rsc_payload_error____", "");
81
+ const jsx = await getErrorJSX(reqPath, { ...req.query }, req.body.error);
61
82
  const manifest = readFileSync(
62
83
  path.resolve(process.cwd(), "____public____/react-client-manifest.json"),
63
84
  "utf8"
@@ -74,11 +95,15 @@ app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
74
95
  // Render HTML via child process, returning a stream
75
96
  function renderAppToHtml(reqPath, paramsString) {
76
97
  return new Promise((resolve, reject) => {
77
- const child = spawn("node", [
78
- path.resolve(__dirname, "render-html.js"),
79
- reqPath,
80
- paramsString,
81
- ]);
98
+ const child = spawn(
99
+ "node",
100
+ [path.resolve(__dirname, "render-html.js"), reqPath, paramsString],
101
+ {
102
+ env: {
103
+ ...process.env,
104
+ },
105
+ }
106
+ );
82
107
 
83
108
  let errorOutput = "";
84
109
  child.stderr.on("data", (data) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou",
3
- "version": "1.4.3",
3
+ "version": "1.6.0",
4
4
  "description": "Minimal React 19 Framework",
5
5
  "main": "index.js",
6
6
  "bin": {
package/webpack.config.js CHANGED
@@ -29,10 +29,14 @@ module.exports = {
29
29
  isDevelopment && "webpack-hot-middleware/client?reload=true",
30
30
  path.resolve(__dirname, "./dinou/client.jsx"),
31
31
  ].filter(Boolean),
32
+ error: [
33
+ isDevelopment && "webpack-hot-middleware/client?reload=true",
34
+ path.resolve(__dirname, "./dinou/client-error.jsx"),
35
+ ].filter(Boolean),
32
36
  },
33
37
  output: {
34
38
  path: path.resolve(process.cwd(), "./____public____"),
35
- filename: "main.js",
39
+ filename: "[name].js",
36
40
  publicPath: "/",
37
41
  clean: true,
38
42
  },