dinou 5.1.1 → 5.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [5.2.0] - 2026-07-13
9
+
10
+ ### Added
11
+ - **Lightweight Plugin System & `dinou.config.js` support**: Added native support for extending the framework without manually modifying the core Express server.
12
+ * **Config Loader**: Automatically loads a `dinou.config.js` file at the root of the project to initialize custom plugins.
13
+ * **`onServerInit(app)` Hook**: Allows plugins to register global Express middlewares, custom endpoints (like webhooks), or API routes.
14
+ * **`onRequestContext(req, res, context)` Hook**: Allows plugins to inject custom properties (like `locale` or `userId`) into the RSC context across standard page requests, Server Actions, and dynamic compilation child processes.
15
+ * **Try-Catch Isolation**: Wrapped plugin hooks in safe boundaries with named logging to prevent third-party plugin errors from crashing the server.
16
+
17
+ ### Fixed
18
+ - **URL resolution for search params**: Fixed `useSearchParams` and `usePathname` to work correctly with URL hashes.
19
+ - **Server Actions with FormData**: Fixed server actions to correctly handle FormData arguments, supporting formData in any position in the args list.
20
+
21
+ ## [5.1.2] - 2026-07-12
22
+
23
+ ### Fixed
24
+ - **Client Link Import Resolution**: Fixed `TypeError: Failed to resolve module specifier 'dinou/core/link.jsx'` client-side crashes occurring when importing and rendering `<Link>` from `"dinou"` inside Server Components under Esbuild and Rollup compilation environments.
25
+
8
26
  ## [5.1.1] - 2026-07-12
9
27
 
10
28
  ### Fixed
@@ -126,7 +126,7 @@ export function usePathname() {
126
126
  return "";
127
127
  }
128
128
 
129
- const path = fullRoute.split("?")[0];
129
+ const path = fullRoute.split("#")[0].split("?")[0];
130
130
  return normalizePath(path);
131
131
  }
132
132
 
@@ -165,6 +165,6 @@ export function useSearchParams() {
165
165
 
166
166
  if (typeof fullRoute !== "string") return new URLSearchParams();
167
167
 
168
- const searchPart = fullRoute.split("?")[1] || "";
168
+ const searchPart = (fullRoute.split("?")[1] || "").split("#")[0];
169
169
  return new URLSearchParams(searchPart);
170
170
  }
@@ -22,13 +22,17 @@ function createServerFunctionProxy(id) {
22
22
  "x-server-function-call": "1",
23
23
  };
24
24
 
25
- if (args[0] instanceof FormData) {
26
- const formData = args[0];
25
+ const formDataIndex = args.findIndex((arg) => arg instanceof FormData);
26
+
27
+ if (formDataIndex !== -1) {
28
+ const formData = args[formDataIndex];
27
29
 
28
30
  formData.append("__dinou_func_id", id);
31
+ formData.append("__dinou_formData_index", String(formDataIndex));
29
32
 
30
33
  if (args.length > 1) {
31
- formData.append("__dinou_args", JSON.stringify(args.slice(1)));
34
+ const serializedArgs = args.map((arg, i) => (i === formDataIndex ? null : arg));
35
+ formData.append("__dinou_args", JSON.stringify(serializedArgs));
32
36
  }
33
37
 
34
38
  body = formData;
@@ -23,13 +23,17 @@ export function createServerFunctionProxy(id) {
23
23
  "x-server-function-call": "1",
24
24
  };
25
25
 
26
- if (args[0] instanceof FormData) {
27
- const formData = args[0];
26
+ const formDataIndex = args.findIndex((arg) => arg instanceof FormData);
27
+
28
+ if (formDataIndex !== -1) {
29
+ const formData = args[formDataIndex];
28
30
 
29
31
  formData.append("__dinou_func_id", id);
32
+ formData.append("__dinou_formData_index", String(formDataIndex));
30
33
 
31
34
  if (args.length > 1) {
32
- formData.append("__dinou_args", JSON.stringify(args.slice(1)));
35
+ const serializedArgs = args.map((arg, i) => (i === formDataIndex ? null : arg));
36
+ formData.append("__dinou_args", JSON.stringify(serializedArgs));
33
37
  }
34
38
 
35
39
  body = formData;
@@ -72,6 +72,18 @@ addHook({
72
72
  });
73
73
  const importModule = require("./import-module");
74
74
  const generateStatic = require("./generate-static.js");
75
+
76
+ // Load Dinou configuration and plugins
77
+ let dinouConfig = { plugins: [] };
78
+ const dinouConfigPath = path.resolve(process.cwd(), "dinou.config.js");
79
+ if (existsSync(dinouConfigPath)) {
80
+ try {
81
+ dinouConfig = require(dinouConfigPath);
82
+ } catch (err) {
83
+ console.error("[Dinou] Error loading dinou.config.js:", err);
84
+ }
85
+ }
86
+
75
87
  const renderAppToHtml = require("./render-app-to-html.js");
76
88
  const { revalidating, regenerating } = require("./revalidating.js");
77
89
  const isDevelopment = process.env.NODE_ENV !== "production";
@@ -283,6 +295,21 @@ const cookieParser = require("cookie-parser");
283
295
  const appUseCookieParser = cookieParser();
284
296
  const app = express();
285
297
  app.use(appUseCookieParser);
298
+
299
+ // Run plugins onServerInit hook
300
+ if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
301
+ for (const plugin of dinouConfig.plugins) {
302
+ if (typeof plugin.onServerInit === "function") {
303
+ try {
304
+ plugin.onServerInit(app);
305
+ console.log(`[Dinou] Plugin initialized: "${plugin.name || "unnamed"}"`);
306
+ } catch (err) {
307
+ console.error(`[Dinou] Error initializing plugin "${plugin.name || "unnamed"}":`, err);
308
+ }
309
+ }
310
+ }
311
+ }
312
+
286
313
  app.use(express.json());
287
314
 
288
315
  // ============================================================
@@ -406,6 +433,20 @@ function getContext(req, res) {
406
433
  redirect: (...args) => safeResCall("redirect", ...args),
407
434
  },
408
435
  };
436
+
437
+ // Run plugins onRequestContext hook
438
+ if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
439
+ for (const plugin of dinouConfig.plugins) {
440
+ if (typeof plugin.onRequestContext === "function") {
441
+ try {
442
+ plugin.onRequestContext(req, res, context);
443
+ } catch (err) {
444
+ console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
445
+ }
446
+ }
447
+ }
448
+ }
449
+
409
450
  return context;
410
451
  }
411
452
 
@@ -534,6 +575,20 @@ function getContextForServerFunctionEndpoint(req, res) {
534
575
  },
535
576
  },
536
577
  };
578
+
579
+ // Run plugins onRequestContext hook
580
+ if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
581
+ for (const plugin of dinouConfig.plugins) {
582
+ if (typeof plugin.onRequestContext === "function") {
583
+ try {
584
+ plugin.onRequestContext(req, res, context);
585
+ } catch (err) {
586
+ console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
587
+ }
588
+ }
589
+ }
590
+ }
591
+
537
592
  return context;
538
593
  }
539
594
 
@@ -1084,6 +1139,20 @@ app.get(/^\/.*\/?$/, async (req, res) => {
1084
1139
  },
1085
1140
  // Do not include res here
1086
1141
  };
1142
+
1143
+ // Run plugins onRequestContext hook
1144
+ if (dinouConfig.plugins && Array.isArray(dinouConfig.plugins)) {
1145
+ for (const plugin of dinouConfig.plugins) {
1146
+ if (typeof plugin.onRequestContext === "function") {
1147
+ try {
1148
+ plugin.onRequestContext(req, res, contextForChild);
1149
+ } catch (err) {
1150
+ console.error(`[Dinou] Error running onRequestContext in plugin "${plugin.name || "unnamed"}":`, err);
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+
1087
1156
  processLimiter
1088
1157
  .run(async () => {
1089
1158
  const isDynamic = true;
@@ -1211,7 +1280,12 @@ app.post("/____server_function____", async (req, res) => {
1211
1280
  const formData = new FormData();
1212
1281
 
1213
1282
  for (const key in req.body) {
1214
- if (key === "__dinou_func_id" || key === "__dinou_args") continue;
1283
+ if (
1284
+ key === "__dinou_func_id" ||
1285
+ key === "__dinou_args" ||
1286
+ key === "__dinou_formData_index"
1287
+ )
1288
+ continue;
1215
1289
  formData.append(key, req.body[key]);
1216
1290
  }
1217
1291
 
@@ -1222,15 +1296,22 @@ app.post("/____server_function____", async (req, res) => {
1222
1296
  }
1223
1297
  }
1224
1298
 
1225
- args = [formData];
1226
-
1227
1299
  if (req.body.__dinou_args) {
1228
1300
  try {
1229
- const extraArgs = JSON.parse(req.body.__dinou_args);
1230
- args.push(...extraArgs);
1301
+ const parsedArgs = JSON.parse(req.body.__dinou_args);
1302
+ if (req.body.__dinou_formData_index !== undefined) {
1303
+ const formDataIndex = parseInt(req.body.__dinou_formData_index, 10);
1304
+ parsedArgs[formDataIndex] = formData;
1305
+ args = parsedArgs;
1306
+ } else {
1307
+ args = [formData, ...parsedArgs];
1308
+ }
1231
1309
  } catch (e) {
1232
- console.error("Error parsing extra args in multipart request");
1310
+ console.error("Error parsing extra args in multipart request", e);
1311
+ args = [formData];
1233
1312
  }
1313
+ } else {
1314
+ args = [formData];
1234
1315
  }
1235
1316
  } else {
1236
1317
  id = req.body.id;
@@ -18,6 +18,10 @@ const absPathToClientRedirect = path.resolve(
18
18
  __dirname,
19
19
  "../core/client-redirect.jsx"
20
20
  );
21
+ const absPathToLink = path.resolve(
22
+ __dirname,
23
+ "../core/link.jsx"
24
+ );
21
25
 
22
26
  const frameworkEntryPoints = {
23
27
  main: path.resolve(__dirname, "../core/client.jsx"),
@@ -32,6 +36,7 @@ const frameworkEntryPoints = {
32
36
  "react-refresh/react-refresh-entry.js"
33
37
  ),
34
38
  dinouClientRedirect: absPathToClientRedirect,
39
+ dinouLink: absPathToLink,
35
40
  };
36
41
 
37
42
  try {
@@ -46,6 +51,12 @@ try {
46
51
  true,
47
52
  manifest
48
53
  );
54
+ updateManifestForModule(
55
+ absPathToLink,
56
+ await fs.readFile(absPathToLink, "utf8"),
57
+ true,
58
+ manifest
59
+ );
49
60
 
50
61
  const componentEntryPoints = [...esbuildEntries].reduce(
51
62
  (acc, dCE) => ({ ...acc, [dCE.outfileName]: dCE.absPath }),
@@ -26,6 +26,10 @@ const absPathToClientRedirect = path.resolve(
26
26
  __dirname,
27
27
  "../core/client-redirect.jsx"
28
28
  );
29
+ const absPathToLink = path.resolve(
30
+ __dirname,
31
+ "../core/link.jsx"
32
+ );
29
33
 
30
34
  const frameworkEntryPoints = {
31
35
  main: path.resolve(__dirname, "../core/client.jsx"),
@@ -40,6 +44,7 @@ const frameworkEntryPoints = {
40
44
  "react-refresh/react-refresh-entry.js"
41
45
  ),
42
46
  dinouClientRedirect: absPathToClientRedirect,
47
+ dinouLink: absPathToLink,
43
48
  };
44
49
 
45
50
  const changedIds = new Set();
@@ -70,6 +75,12 @@ async function updateEntriesAndComponents() {
70
75
  true,
71
76
  manifest
72
77
  );
78
+ updateManifestForModule(
79
+ absPathToLink,
80
+ await fs.readFile(absPathToLink, "utf8"),
81
+ true,
82
+ manifest
83
+ );
73
84
 
74
85
  currentServerFiles = new Set(
75
86
  serverFiles.map((f) => normalizePath(path.resolve(f)))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou-ejected",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "main": "index.js",
5
5
  "private": true,
6
6
  "exports": {
@@ -52,6 +52,10 @@ module.exports = async function () {
52
52
  __dirname,
53
53
  "../core/client-redirect.jsx",
54
54
  ),
55
+ dinouLink: path.resolve(
56
+ __dirname,
57
+ "../core/link.jsx",
58
+ ),
55
59
  }
56
60
  : {
57
61
  main: path.resolve(__dirname, "../core/client.jsx"),
@@ -64,6 +68,10 @@ module.exports = async function () {
64
68
  __dirname,
65
69
  "../core/client-redirect.jsx",
66
70
  ),
71
+ dinouLink: path.resolve(
72
+ __dirname,
73
+ "../core/link.jsx",
74
+ ),
67
75
  },
68
76
  output: {
69
77
  dir: outputDirectory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "Lightweight-Ejectable Full-Stack React 19 Framework with support for React Server Components (RSC), Server Functions, Server-Side Rendering (SSR), Static Generation (SSG), Incremental Static Generation (ISG), and Incremental Static Regeneration (ISR)",
5
5
  "main": "index.js",
6
6
  "bin": {