dinou 4.0.15 → 5.0.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +150 -57
  3. package/dinou/core/babel-esm-loader.js +87 -5
  4. package/dinou/core/build-static-pages.js +17 -97
  5. package/dinou/core/generate-static-page.js +21 -3
  6. package/dinou/core/generate-static-pages.js +30 -41
  7. package/dinou/core/generate-static-rsc.js +11 -5
  8. package/dinou/core/generate-static-rscs.js +11 -5
  9. package/dinou/core/generate-static.js +2 -3
  10. package/dinou/core/get-jsx.js +2 -1
  11. package/dinou/core/get-ssg-metadata.js +5 -13
  12. package/dinou/core/import-module.js +25 -6
  13. package/dinou/core/redirect.jsx +18 -11
  14. package/dinou/core/register-loader.mjs +1 -0
  15. package/dinou/core/render-app-to-html.js +209 -35
  16. package/dinou/core/render-html.js +144 -26
  17. package/dinou/core/render-jsx-to-client-jsx.js +6 -7
  18. package/dinou/core/request-context.js +11 -6
  19. package/dinou/core/revalidating.js +29 -25
  20. package/dinou/core/server.js +279 -88
  21. package/dinou/core/url-resolver.js +3 -0
  22. package/dinou/esbuild/dev.mjs +31 -2
  23. package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +0 -40
  24. package/dinou/index.d.ts +1 -1
  25. package/dinou/index.mjs +31 -10
  26. package/dinou/package.json +1 -1
  27. package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +88 -49
  28. package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +7 -9
  29. package/dinou/rollup/rollup.config.js +1 -2
  30. package/dinou/webpack/helpers/get-webpack-entries.js +0 -40
  31. package/package.json +3 -3
  32. package/dinou/core/babel-plugin-register-imports.js +0 -50
  33. package/dinou/core/get-ssg-jsx-or-jsx.js +0 -14
  34. package/dinou/core/get-ssg-jsx.js +0 -83
  35. package/dinou/core/jsx-json.js +0 -55
  36. package/dinou/core/register-hooks.js +0 -18
@@ -1,17 +1,53 @@
1
1
  require("dotenv/config");
2
+ const Module = require("module");
3
+ const originalResolveFilename = Module._resolveFilename;
4
+ const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
5
+ globalThis.__dinou_require__ = require;
6
+ const path = require("path");
7
+
8
+ let reactServerPath, reactDomServerPath, reactJsxRuntimePath, reactJsxDevRuntimePath;
9
+
10
+ if (!isWebpack) {
11
+ const reactPkgJson = require.resolve("react/package.json");
12
+ reactServerPath = path.join(path.dirname(reactPkgJson), "react.react-server.js");
13
+ reactJsxRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-runtime.react-server.js");
14
+ reactJsxDevRuntimePath = path.join(path.dirname(reactPkgJson), "jsx-dev-runtime.react-server.js");
15
+
16
+ const reactDomPkgJson = require.resolve("react-dom/package.json");
17
+ reactDomServerPath = path.join(path.dirname(reactDomPkgJson), "react-dom.react-server.js");
18
+ }
19
+
20
+ Module._resolveFilename = function (request, parent, isMain, options) {
21
+ if (!isWebpack) {
22
+ if (request === "react") {
23
+ return reactServerPath;
24
+ } else if (request === "react-dom") {
25
+ return reactDomServerPath;
26
+ } else if (request === "react/jsx-runtime") {
27
+ return reactJsxRuntimePath;
28
+ } else if (request === "react/jsx-dev-runtime") {
29
+ return reactJsxDevRuntimePath;
30
+ }
31
+ }
32
+ return originalResolveFilename.call(this, request, parent, isMain, options);
33
+ };
34
+
2
35
  require("./register-paths");
3
- require("./register-hooks.js");
4
36
  const webpackRegister = require("react-server-dom-webpack/node-register");
5
- const path = require("path");
6
37
  const { readFileSync, existsSync, createReadStream } = require("fs");
7
- const { renderToPipeableStream } = require("react-server-dom-webpack/server");
38
+ // const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
39
+ const { renderToPipeableStream } = isWebpack
40
+ ? require("react-server-dom-webpack/server")
41
+ : require("@roggc/react-server-dom-esm/server");
8
42
  const express = require("express");
9
43
  const getJSX = require("./get-jsx.js");
44
+ const { getFilePathAndDynamicParams } = require("./get-file-path-and-dynamic-params.js");
10
45
  const { getErrorJSX } = require("./get-error-jsx.js");
11
46
  const addHook = require("./asset-require-hook.js");
12
47
  const { extensions } = require("./asset-extensions.js");
13
- webpackRegister();
14
- const babelPluginRegisterImports = require("./babel-plugin-register-imports.js");
48
+ if (isWebpack) {
49
+ webpackRegister();
50
+ }
15
51
  const babelRegister = require("@babel/register");
16
52
  babelRegister({
17
53
  ignore: [/node_modules[\\/](?!dinou)/],
@@ -19,7 +55,7 @@ babelRegister({
19
55
  ["@babel/preset-react", { runtime: "automatic" }],
20
56
  "@babel/preset-typescript",
21
57
  ],
22
- plugins: [babelPluginRegisterImports, "@babel/transform-modules-commonjs"],
58
+ plugins: ["@babel/transform-modules-commonjs"],
23
59
  extensions: [".js", ".jsx", ".ts", ".tsx"],
24
60
  });
25
61
  const createScopedName = require("./createScopedName");
@@ -39,8 +75,7 @@ const { revalidating, regenerating } = require("./revalidating.js");
39
75
  const isDevelopment = process.env.NODE_ENV !== "production";
40
76
  const outputFolder = isDevelopment ? "public" : "dist3";
41
77
  const chokidar = require("chokidar");
42
- const { fileURLToPath } = require("url");
43
- const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
78
+ const { fileURLToPath, pathToFileURL } = require("url");
44
79
  const parseExports = require("./parse-exports.js");
45
80
  const { requestStorage } = require("./request-context.js");
46
81
  const { useServerRegex } = require("../constants.js");
@@ -248,6 +283,35 @@ const appUseCookieParser = cookieParser();
248
283
  const app = express();
249
284
  app.use(appUseCookieParser);
250
285
  app.use(express.json());
286
+
287
+ // ============================================================
288
+ // 🛡️ ANTI-BOT SHIELD (Prevent ISG from collapsing)
289
+ // ============================================================
290
+ const botGarbagePatterns = [
291
+ /\.php$/i, // Any PHP file
292
+ /\.env$/i, // Attempts to steal environment variables
293
+ /\.git\b/i, // Attempts to access Git repository
294
+ /wp-admin/i, // WordPress admin
295
+ /wp-content/i, // WordPress content
296
+ /wp-includes/i, // WordPress core files
297
+ /xmlrpc\.php/i, // WordPress DDoS attacks
298
+ /\.sql$/i, // Attempts to download database dumps
299
+ /\.asp$/i, // Legacy ASP pages
300
+ /\.jsp$/i, // JSP pages
301
+ /\.cgi$/i, // Old CGI scripts
302
+ /\.bak$/i, // Backup files
303
+ /\.log$/i, // Log files
304
+ ];
305
+
306
+ app.use((req, res, next) => {
307
+ const isGarbage = botGarbagePatterns.some((pattern) => pattern.test(req.path));
308
+ if (isGarbage) {
309
+ // Return 404 immediately to prevent the ISG engine from spawning processes
310
+ return res.status(404).send("Not Found");
311
+ }
312
+ next();
313
+ });
314
+
251
315
  const { resolveRelativeUrl } = require("./url-resolver");
252
316
 
253
317
  function getContext(req, res) {
@@ -372,7 +436,7 @@ function getContextForServerFunctionEndpoint(req, res) {
372
436
  if (referer) {
373
437
  try {
374
438
  refererPath = new URL(referer).pathname;
375
- } catch (e) {}
439
+ } catch (e) { }
376
440
  }
377
441
  const resolvedUrl = resolveRelativeUrl(rawUrl, refererPath);
378
442
  let finalUrl = "/";
@@ -447,8 +511,6 @@ function getContextForServerFunctionEndpoint(req, res) {
447
511
  },
448
512
 
449
513
  clearCookie: (name, options) => {
450
- const path = options?.path || "/";
451
-
452
514
  // SCENARIO A: Native
453
515
  if (!res.headersSent) {
454
516
  res.setHeader("Content-Type", "text/x-component");
@@ -457,7 +519,15 @@ function getContextForServerFunctionEndpoint(req, res) {
457
519
  }
458
520
 
459
521
  // SCENARIO B: Custom stream command
460
- const cookieStr = `${name}=; Max-Age=0; path=${path};`;
522
+ let cookieStr = `${name}=; Max-Age=0`;
523
+ const path = options?.path || "/";
524
+ cookieStr += `; path=${path}`;
525
+ if (options) {
526
+ if (options.domain) cookieStr += `; domain=${options.domain}`;
527
+ if (options.secure) cookieStr += `; secure`;
528
+ if (options.sameSite) cookieStr += `; samesite=${options.sameSite}`;
529
+ }
530
+ cookieStr += ";";
461
531
  const safeCookieStr = JSON.stringify(cookieStr);
462
532
  res.write(`D:{"type":"cookie","cookie":${safeCookieStr}}\n`);
463
533
  },
@@ -468,12 +538,27 @@ function getContextForServerFunctionEndpoint(req, res) {
468
538
 
469
539
  app.use(express.static(path.resolve(process.cwd(), outputFolder)));
470
540
 
541
+ const clientManifestResolvedPath = path.resolve(
542
+ process.cwd(),
543
+ isWebpack
544
+ ? `${outputFolder}/react-client-manifest.json`
545
+ : `react_client_manifest/react-client-manifest.json`,
546
+ );
547
+
548
+ function isManifestReady() {
549
+ try {
550
+ return existsSync(clientManifestResolvedPath) && readFileSync(clientManifestResolvedPath, "utf8").trim().length > 2;
551
+ } catch (e) {
552
+ return false;
553
+ }
554
+ }
555
+
471
556
  let isReady = isDevelopment; // In dev we are always ready (or almost)
472
557
 
473
558
  app.get("/__DINOU_STATUS_PLAYWRIGHT__", (req, res) => {
474
559
  res.json({
475
560
  status: "ok",
476
- isReady,
561
+ isReady: isDevelopment ? isManifestReady() : isReady,
477
562
  mode: isDevelopment ? "development" : "production",
478
563
  });
479
564
  });
@@ -548,16 +633,17 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
548
633
  }
549
634
  if (existsSync(payloadPath)) {
550
635
  res.setHeader("Content-Type", "application/octet-stream");
551
- const readStream = createReadStream(payloadPath);
552
- readStream.on("error", (err) => {
636
+ try {
637
+ const buffer = readFileSync(payloadPath);
638
+ return res.send(buffer);
639
+ } catch (err) {
553
640
  console.error("Error reading RSC file:", err);
554
- res.status(500).end();
555
- });
556
- return readStream.pipe(res);
641
+ return res.status(500).end();
642
+ }
557
643
  }
558
644
  }
559
645
  const context = getContext(req, res);
560
- const isNotFound = null;
646
+ const isNotFound = {};
561
647
  await requestStorage.run(context, async () => {
562
648
  const jsx = await getJSX(
563
649
  reqPath,
@@ -566,6 +652,10 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
566
652
  isDevelopment,
567
653
  );
568
654
 
655
+ if (isNotFound.value) {
656
+ res.status(404);
657
+ }
658
+
569
659
  if (res.headersSent) {
570
660
  return;
571
661
  }
@@ -584,7 +674,9 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
584
674
  )
585
675
  : cachedClientManifest;
586
676
 
587
- const { pipe } = renderToPipeableStream(jsx, manifest);
677
+ const { pipe } = isWebpack
678
+ ? renderToPipeableStream(jsx, manifest)
679
+ : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
588
680
  pipe(res);
589
681
  });
590
682
  } catch (error) {
@@ -633,7 +725,9 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
633
725
  ),
634
726
  )
635
727
  : cachedClientManifest;
636
- const { pipe } = renderToPipeableStream(jsx, manifest);
728
+ const { pipe } = isWebpack
729
+ ? renderToPipeableStream(jsx, manifest)
730
+ : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
637
731
  pipe(res);
638
732
  } catch (error) {
639
733
  console.error("Error rendering RSC:", error);
@@ -641,9 +735,116 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
641
735
  }
642
736
  });
643
737
 
644
- app.get(/^\/.*\/?$/, (req, res) => {
738
+ const pageFunctionsConfigCache = new Map();
739
+
740
+ app.get(/^\/.*\/?$/, async (req, res) => {
645
741
  try {
742
+ const reqSegments = req.path.split("/").filter(Boolean);
743
+ const srcFolder = path.resolve(process.cwd(), "src");
744
+ const [pagePath, dynamicParams] = getFilePathAndDynamicParams(
745
+ reqSegments,
746
+ req.query,
747
+ srcFolder,
748
+ );
749
+
750
+ if (!pagePath) {
751
+ if (path.extname(req.path)) {
752
+ return res.status(404).send("Not Found");
753
+ }
754
+ }
755
+
756
+ let isPathBlocked = false;
757
+
758
+ if (pagePath) {
759
+ let cachedConfig = pageFunctionsConfigCache.get(pagePath);
760
+ if (!cachedConfig) {
761
+ const pageFolder = path.dirname(pagePath);
762
+ const [pageFunctionsPath] = getFilePathAndDynamicParams(
763
+ reqSegments,
764
+ req.query,
765
+ pageFolder,
766
+ "page_functions",
767
+ true,
768
+ true,
769
+ undefined,
770
+ reqSegments.length,
771
+ );
772
+
773
+ if (pageFunctionsPath) {
774
+ const pageFunctionsModule = await importModule(pageFunctionsPath);
775
+ const allowISGValue = pageFunctionsModule.allowISG
776
+ ? await pageFunctionsModule.allowISG()
777
+ : true;
778
+
779
+ let staticPathsSet = null;
780
+ if (pageFunctionsModule.getStaticPaths) {
781
+ const paths = await pageFunctionsModule.getStaticPaths();
782
+ staticPathsSet = new Set(
783
+ (paths || []).map((pathObj) => {
784
+ const sortedEntries = Object.entries(pathObj).sort((a, b) =>
785
+ a[0].localeCompare(b[0])
786
+ );
787
+ return JSON.stringify(sortedEntries);
788
+ })
789
+ );
790
+ }
791
+
792
+ cachedConfig = {
793
+ allowISG: allowISGValue,
794
+ staticPathsSet,
795
+ validateParams: pageFunctionsModule.validateParams || null,
796
+ };
797
+ } else {
798
+ cachedConfig = {
799
+ allowISG: true,
800
+ staticPathsSet: null,
801
+ validateParams: null,
802
+ };
803
+ }
804
+
805
+ if (!isDevelopment) {
806
+ pageFunctionsConfigCache.set(pagePath, cachedConfig);
807
+ }
808
+ }
809
+
810
+ const { allowISG: allowISGValue, staticPathsSet, validateParams: validateParamsFn } = cachedConfig;
811
+ const hasParams = Object.keys(dynamicParams || {}).length > 0;
812
+ if (hasParams) {
813
+ if (validateParamsFn) {
814
+ const isValid = await validateParamsFn(dynamicParams);
815
+ if (!isValid) {
816
+ isPathBlocked = true;
817
+ }
818
+ }
819
+
820
+ if (!isPathBlocked && allowISGValue === false) {
821
+ let isPathAllowed = false;
822
+ if (staticPathsSet) {
823
+ const sortedQueryEntries = Object.entries(dynamicParams)
824
+ .sort((a, b) => a[0].localeCompare(b[0]))
825
+ .map(([k, v]) => {
826
+ if (Array.isArray(v)) return [k, v.join(",")];
827
+ return [k, String(v)];
828
+ });
829
+ const serializedQuery = JSON.stringify(sortedQueryEntries);
830
+ isPathAllowed = staticPathsSet.has(serializedQuery);
831
+ }
832
+ if (!isPathAllowed) {
833
+ if (isDevelopment) {
834
+ isPathBlocked = true;
835
+ } else {
836
+ const htmlPath = path.join("dist2", req.path, "index.html");
837
+ if (!existsSync(htmlPath)) {
838
+ isPathBlocked = true;
839
+ }
840
+ }
841
+ }
842
+ }
843
+ }
844
+ }
845
+
646
846
  const reqPath = req.path.endsWith("/") ? req.path : req.path + "/";
847
+
647
848
  // 1. Correct Map initialization
648
849
  if (!isDynamic.has(reqPath)) {
649
850
  // Initialize with a mutable object.
@@ -654,7 +855,7 @@ app.get(/^\/.*\/?$/, (req, res) => {
654
855
  // Get reference to the mutable object
655
856
  const dynamicState = isDynamic.get(reqPath);
656
857
  // console.log("dynamicState.value", dynamicState.value);
657
- if (!isDevelopment && !dynamicState.value) {
858
+ if (!isDevelopment && !dynamicState.value && pagePath && !isPathBlocked) {
658
859
  revalidating(reqPath, dynamicState);
659
860
  let htmlPathOld;
660
861
  if (regenerating.has(reqPath)) {
@@ -674,29 +875,19 @@ app.get(/^\/.*\/?$/, (req, res) => {
674
875
  } else {
675
876
  res.statusCode = 200; // Default
676
877
  }
677
- const rStream = createReadStream(fileToRead);
678
-
679
- rStream.on("error", (err) => {
680
- console.error("Error reading HTML file:", err);
681
- if (!res.headersSent) res.status(500).send("Server Error");
682
- });
683
-
684
- // 1. IMPORTANT: We use { end: false } so pipe doesn't close the response
685
- rStream.pipe(res, { end: false });
686
-
687
- // 2. When the file finishes sending...
688
- rStream.on("end", () => {
689
- // 3. Write our additional content
878
+ try {
879
+ const htmlContent = readFileSync(fileToRead);
880
+ res.write(htmlContent);
690
881
  if (htmlPathOld) {
691
882
  res.write("<script>window.__DINOU_USE_OLD_RSC__=true;</script>");
692
883
  }
693
884
  res.write("<script>window.__DINOU_USE_STATIC__=true;</script>");
694
-
695
- // 4. Manually close the response (now we do)
696
885
  res.end();
697
- });
698
-
699
- return; // The stream is already flowing
886
+ } catch (err) {
887
+ console.error("Error reading HTML file:", err);
888
+ if (!res.headersSent) res.status(500).send("Server Error");
889
+ }
890
+ return;
700
891
  }
701
892
  }
702
893
 
@@ -733,6 +924,7 @@ app.get(/^\/.*\/?$/, (req, res) => {
733
924
  res,
734
925
  capturedStatus,
735
926
  isDynamic,
927
+ isPathBlocked,
736
928
  );
737
929
 
738
930
  res.setHeader("Content-Type", "text/html");
@@ -962,57 +1154,56 @@ app.post("/____server_function____", async (req, res) => {
962
1154
  return res.status(400).json({ error: "Export is not a function" });
963
1155
  }
964
1156
 
965
- await processLimiter.run(async () => {
966
- const context = getContextForServerFunctionEndpoint(req, res);
1157
+ const context = getContextForServerFunctionEndpoint(req, res);
967
1158
 
968
- let result;
969
- try {
970
- result = await requestStorage.run(context, async () => {
971
- return await fn(...args);
972
- });
973
- } catch (err) {
974
- // 💡 WE INTERCEPT THE REDIRECT
975
- if (err && err.$$type === "dinou-internal-redirect") {
976
- const safeUrl = JSON.stringify(err.url);
977
-
978
- if (!res.headersSent) {
979
- // SCENARIO A: Clean (Content-Type application/json)
980
- res.setHeader("Content-Type", "application/json");
981
- res.setHeader("X-Dinou-Redirect", err.url);
982
- return res.status(200).json({ redirect: err.url });
983
- } else {
984
- // SCENARIO B: Dirty/Active Stream
985
- // Write a custom line-based stream command
986
- res.write(`D:{"type":"redirect","url":${safeUrl}}\n`);
987
-
988
- // ⚠️ IMPORTANT:
989
- // 1. We close the response, since we redirected and there will be no RSC payload.
990
- res.end();
991
-
992
- // 2. WE STOP execution so it doesn't continue to res.json() below.
993
- return;
994
- }
1159
+ let result;
1160
+ try {
1161
+ result = await requestStorage.run(context, async () => {
1162
+ return await fn(...args);
1163
+ });
1164
+ } catch (err) {
1165
+ // 💡 WE INTERCEPT THE REDIRECT
1166
+ if (err && err.$$type === "dinou-internal-redirect") {
1167
+ const safeUrl = JSON.stringify(err.url);
1168
+
1169
+ if (!res.headersSent) {
1170
+ // SCENARIO A: Clean (Content-Type application/json)
1171
+ res.setHeader("Content-Type", "application/json");
1172
+ res.setHeader("X-Dinou-Redirect", err.url);
1173
+ return res.status(200).json({ redirect: err.url });
1174
+ } else {
1175
+ // SCENARIO B: Dirty/Active Stream
1176
+ // Write a custom line-based stream command
1177
+ res.write(`D:{"type":"redirect","url":${safeUrl}}\n`);
1178
+
1179
+ // ⚠️ IMPORTANT:
1180
+ // 1. We close the response, since we redirected and there will be no RSC payload.
1181
+ // 2. WE STOP execution so it doesn't continue to res.json() below.
1182
+ res.end();
1183
+ return;
995
1184
  }
996
- throw err; // If it's another error, throw it to the outer catch
997
1185
  }
1186
+ throw err; // If it's another error, throw it to the outer catch
1187
+ }
998
1188
 
999
- if (!res.headersSent) res.setHeader("Content-Type", "text/x-component");
1000
- const manifestPath = path.resolve(
1001
- process.cwd(),
1002
- isWebpack
1003
- ? `${outputFolder}/react-client-manifest.json`
1004
- : `react_client_manifest/react-client-manifest.json`,
1005
- );
1006
- // Verify that the manifest exists to avoid errors
1007
- if (!existsSync(manifestPath)) {
1008
- return res.status(500).json({ error: "Manifest not found" });
1009
- }
1010
- const manifest = isDevelopment
1011
- ? JSON.parse(readFileSync(manifestPath, "utf8"))
1012
- : cachedClientManifest;
1013
- const { pipe } = renderToPipeableStream(result, manifest);
1014
- pipe(res);
1015
- });
1189
+ if (!res.headersSent) res.setHeader("Content-Type", "text/x-component");
1190
+ const manifestPath = path.resolve(
1191
+ process.cwd(),
1192
+ isWebpack
1193
+ ? `${outputFolder}/react-client-manifest.json`
1194
+ : `react_client_manifest/react-client-manifest.json`,
1195
+ );
1196
+ // Verify that the manifest exists to avoid errors
1197
+ if (!existsSync(manifestPath)) {
1198
+ return res.status(500).json({ error: "Manifest not found" });
1199
+ }
1200
+ const manifest = isDevelopment
1201
+ ? JSON.parse(readFileSync(manifestPath, "utf8"))
1202
+ : cachedClientManifest;
1203
+ const { pipe } = isWebpack
1204
+ ? renderToPipeableStream(result, manifest)
1205
+ : renderToPipeableStream(result, pathToFileURL(process.cwd()).href + "/");
1206
+ pipe(res);
1016
1207
  } catch (err) {
1017
1208
  console.error(`Server function error [${req.body?.id}]:`, err);
1018
1209
  // In production, do not send full err.message to avoid leaks
@@ -1,4 +1,7 @@
1
1
  function resolveRelativeUrl(href, currentPathname) {
2
+ if (!href || typeof href !== "string") {
3
+ return "/";
4
+ }
2
5
  if (href.startsWith("/") || href.includes("://")) {
3
6
  return href;
4
7
  }
@@ -8,6 +8,7 @@ import { regex as assetRegex } from "../core/asset-extensions.js";
8
8
  import normalizePath from "./helpers-esbuild/normalize-path.mjs";
9
9
  import { fileURLToPath, pathToFileURL } from "url";
10
10
  import { updateManifestForModule } from "./helpers-esbuild/update-manifest-for-module.mjs";
11
+ import { useServerRegex } from "../constants.js";
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
@@ -146,6 +147,16 @@ const debounceRecreateAndReload = () => {
146
147
  }, 300);
147
148
  };
148
149
 
150
+ let reloadTimer = null;
151
+ const debounceReload = () => {
152
+ if (reloadTimer) clearTimeout(reloadTimer);
153
+ reloadTimer = setTimeout(() => {
154
+ if (hmrEngine.value) {
155
+ hmrEngine.value.broadcastMessage({ type: "reload" });
156
+ }
157
+ }, 100);
158
+ };
159
+
149
160
  watcher.on("add", async (file) => {
150
161
  const ext = path.extname(file);
151
162
  if (codeCssRegex.test(ext) || assetRegex.test(ext)) {
@@ -198,7 +209,13 @@ function existsInManifest(resolvedFile, manifest) {
198
209
  watcher.on("change", async (file) => {
199
210
  const resolvedFile = normalizePath(path.resolve(file));
200
211
  const oldManifest = { ...manifest };
212
+ const oldEntryKeys = JSON.stringify(Object.keys(entryPoints).sort());
213
+
201
214
  await updateEntriesAndComponents();
215
+
216
+ const newEntryKeys = JSON.stringify(Object.keys(entryPoints).sort());
217
+ const entryPointsChanged = oldEntryKeys !== newEntryKeys;
218
+
202
219
  // Check if changed file is a client component
203
220
  const isClientModule = clientComponentsPaths.includes(resolvedFile);
204
221
  const isServerModule = currentServerFiles.has(resolvedFile);
@@ -215,6 +232,18 @@ watcher.on("change", async (file) => {
215
232
  changedIds.add(resolvedFile);
216
233
  return;
217
234
  }
218
- // Server module, css module, or other file changed
219
- debounceRecreateAndReload();
235
+
236
+ // If it's a Server Action module ("use server"), do not reload the browser
237
+ const fileContent = await fs.readFile(resolvedFile, "utf8").catch(() => "");
238
+ const isServerAction = useServerRegex.test(fileContent.trim());
239
+ if (isServerAction) {
240
+ return;
241
+ }
242
+
243
+ // Only recreate context and reload if entry points actually changed or it's a CSS file
244
+ if (entryPointsChanged || file.endsWith(".css") || file.endsWith(".scss")) {
245
+ debounceRecreateAndReload();
246
+ } else {
247
+ debounceReload();
248
+ }
220
249
  });
@@ -150,41 +150,6 @@ export default async function getEsbuildEntries({
150
150
  return fileName.startsWith("page.") || fileName.startsWith("layout.");
151
151
  }
152
152
 
153
- function isAsyncDefaultExport(code) {
154
- const ast = parser.parse(code, {
155
- sourceType: "module",
156
- plugins: ["jsx", "typescript"],
157
- });
158
-
159
- let isAsync = false;
160
-
161
- traverse.default(ast, {
162
- ExportDefaultDeclaration(path) {
163
- let decl = path.node.declaration;
164
-
165
- if (decl.type === "Identifier") {
166
- const binding = path.scope.getBinding(decl.name);
167
- if (binding && binding.path) {
168
- decl = binding.path.node;
169
- if (decl.type === "VariableDeclarator") {
170
- decl = decl.init;
171
- }
172
- }
173
- }
174
-
175
- if (
176
- decl &&
177
- (decl.type === "FunctionDeclaration" ||
178
- decl.type === "ArrowFunctionExpression" ||
179
- decl.type === "FunctionExpression")
180
- ) {
181
- isAsync = decl.async;
182
- }
183
- },
184
- });
185
-
186
- return isAsync;
187
- }
188
153
 
189
154
  const files = await glob(["**/*.{js,jsx,ts,tsx}"], {
190
155
  cwd: srcDir,
@@ -222,11 +187,6 @@ export default async function getEsbuildEntries({
222
187
  }
223
188
  });
224
189
  } else if (isPageOrLayout(absPath)) {
225
- if (!isAsyncDefaultExport(code)) {
226
- console.warn(
227
- `[react-client-manifest] The file ${normalizedPath} is a page or layout without "use client" directive, but its default export is not an async function.`,
228
- );
229
- }
230
190
  serverModules.add(normalizedPath);
231
191
  try {
232
192
  const { imports, assets, csss } = await getImportsAndAssetsAndCsss(
package/dinou/index.d.ts CHANGED
@@ -55,7 +55,7 @@ export interface ResponseProxy {
55
55
  * @param name Name of the cookie to clear.
56
56
  * @param options Cookie options (e.g., domain, path).
57
57
  */
58
- clearCookie(name: string, options?: { domain?: string; path?: string }): void;
58
+ clearCookie(name: string, options?: CookieOptions): void;
59
59
 
60
60
  /**
61
61
  * Sends a command to the main process to set an HTTP header.
package/dinou/index.mjs CHANGED
@@ -1,13 +1,34 @@
1
- // dinou/index.mjs
2
- import dinou from "./index.js";
1
+ import { getContext } from "./core/request-context.js";
2
+ import {
3
+ usePathname,
4
+ useSearchParams,
5
+ useRouter,
6
+ useNavigationLoading,
7
+ } from "./core/navigation.js";
8
+ import { redirect } from "./core/redirect.jsx";
9
+ import { ClientRedirect } from "./core/client-redirect.jsx";
10
+ import { Link } from "./core/link.jsx";
3
11
 
4
- export const getContext = dinou.getContext;
5
- export const usePathname = dinou.usePathname;
6
- export const useSearchParams = dinou.useSearchParams;
7
- export const useRouter = dinou.useRouter;
8
- export const useNavigationLoading = dinou.useNavigationLoading;
9
- export const redirect = dinou.redirect;
10
- export const ClientRedirect = dinou.ClientRedirect;
11
- export const Link = dinou.Link;
12
+ const dinou = {
13
+ getContext,
14
+ usePathname,
15
+ useSearchParams,
16
+ useRouter,
17
+ useNavigationLoading,
18
+ redirect,
19
+ ClientRedirect,
20
+ Link,
21
+ };
22
+
23
+ export {
24
+ getContext,
25
+ usePathname,
26
+ useSearchParams,
27
+ useRouter,
28
+ useNavigationLoading,
29
+ redirect,
30
+ ClientRedirect,
31
+ Link,
32
+ };
12
33
 
13
34
  export default dinou;