dinou 4.0.16 → 5.0.1

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 (35) hide show
  1. package/CHANGELOG.md +15 -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/navigation.js +42 -27
  14. package/dinou/core/redirect.jsx +18 -11
  15. package/dinou/core/register-loader.mjs +1 -0
  16. package/dinou/core/render-app-to-html.js +198 -31
  17. package/dinou/core/render-html.js +144 -26
  18. package/dinou/core/render-jsx-to-client-jsx.js +6 -7
  19. package/dinou/core/request-context.js +11 -6
  20. package/dinou/core/revalidating.js +29 -25
  21. package/dinou/core/server.js +270 -85
  22. package/dinou/esbuild/dev.mjs +31 -2
  23. package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +0 -40
  24. package/dinou/index.mjs +31 -10
  25. package/dinou/package.json +1 -1
  26. package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +88 -49
  27. package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +7 -9
  28. package/dinou/rollup/rollup.config.js +1 -2
  29. package/dinou/webpack/helpers/get-webpack-entries.js +0 -40
  30. package/package.json +2 -2
  31. package/dinou/core/babel-plugin-register-imports.js +0 -50
  32. package/dinou/core/get-ssg-jsx-or-jsx.js +0 -14
  33. package/dinou/core/get-ssg-jsx.js +0 -83
  34. package/dinou/core/jsx-json.js +0 -55
  35. 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 = "/";
@@ -474,12 +538,27 @@ function getContextForServerFunctionEndpoint(req, res) {
474
538
 
475
539
  app.use(express.static(path.resolve(process.cwd(), outputFolder)));
476
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
+
477
556
  let isReady = isDevelopment; // In dev we are always ready (or almost)
478
557
 
479
558
  app.get("/__DINOU_STATUS_PLAYWRIGHT__", (req, res) => {
480
559
  res.json({
481
560
  status: "ok",
482
- isReady,
561
+ isReady: isDevelopment ? isManifestReady() : isReady,
483
562
  mode: isDevelopment ? "development" : "production",
484
563
  });
485
564
  });
@@ -554,16 +633,17 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
554
633
  }
555
634
  if (existsSync(payloadPath)) {
556
635
  res.setHeader("Content-Type", "application/octet-stream");
557
- const readStream = createReadStream(payloadPath);
558
- readStream.on("error", (err) => {
636
+ try {
637
+ const buffer = readFileSync(payloadPath);
638
+ return res.send(buffer);
639
+ } catch (err) {
559
640
  console.error("Error reading RSC file:", err);
560
- res.status(500).end();
561
- });
562
- return readStream.pipe(res);
641
+ return res.status(500).end();
642
+ }
563
643
  }
564
644
  }
565
645
  const context = getContext(req, res);
566
- const isNotFound = null;
646
+ const isNotFound = {};
567
647
  await requestStorage.run(context, async () => {
568
648
  const jsx = await getJSX(
569
649
  reqPath,
@@ -572,6 +652,10 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
572
652
  isDevelopment,
573
653
  );
574
654
 
655
+ if (isNotFound.value) {
656
+ res.status(404);
657
+ }
658
+
575
659
  if (res.headersSent) {
576
660
  return;
577
661
  }
@@ -590,7 +674,9 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
590
674
  )
591
675
  : cachedClientManifest;
592
676
 
593
- const { pipe } = renderToPipeableStream(jsx, manifest);
677
+ const { pipe } = isWebpack
678
+ ? renderToPipeableStream(jsx, manifest)
679
+ : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
594
680
  pipe(res);
595
681
  });
596
682
  } catch (error) {
@@ -639,7 +725,9 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
639
725
  ),
640
726
  )
641
727
  : cachedClientManifest;
642
- const { pipe } = renderToPipeableStream(jsx, manifest);
728
+ const { pipe } = isWebpack
729
+ ? renderToPipeableStream(jsx, manifest)
730
+ : renderToPipeableStream(jsx, pathToFileURL(process.cwd()).href + "/");
643
731
  pipe(res);
644
732
  } catch (error) {
645
733
  console.error("Error rendering RSC:", error);
@@ -647,9 +735,116 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
647
735
  }
648
736
  });
649
737
 
650
- app.get(/^\/.*\/?$/, (req, res) => {
738
+ const pageFunctionsConfigCache = new Map();
739
+
740
+ app.get(/^\/.*\/?$/, async (req, res) => {
651
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
+
652
846
  const reqPath = req.path.endsWith("/") ? req.path : req.path + "/";
847
+
653
848
  // 1. Correct Map initialization
654
849
  if (!isDynamic.has(reqPath)) {
655
850
  // Initialize with a mutable object.
@@ -660,7 +855,7 @@ app.get(/^\/.*\/?$/, (req, res) => {
660
855
  // Get reference to the mutable object
661
856
  const dynamicState = isDynamic.get(reqPath);
662
857
  // console.log("dynamicState.value", dynamicState.value);
663
- if (!isDevelopment && !dynamicState.value) {
858
+ if (!isDevelopment && !dynamicState.value && pagePath && !isPathBlocked) {
664
859
  revalidating(reqPath, dynamicState);
665
860
  let htmlPathOld;
666
861
  if (regenerating.has(reqPath)) {
@@ -680,29 +875,19 @@ app.get(/^\/.*\/?$/, (req, res) => {
680
875
  } else {
681
876
  res.statusCode = 200; // Default
682
877
  }
683
- const rStream = createReadStream(fileToRead);
684
-
685
- rStream.on("error", (err) => {
686
- console.error("Error reading HTML file:", err);
687
- if (!res.headersSent) res.status(500).send("Server Error");
688
- });
689
-
690
- // 1. IMPORTANT: We use { end: false } so pipe doesn't close the response
691
- rStream.pipe(res, { end: false });
692
-
693
- // 2. When the file finishes sending...
694
- rStream.on("end", () => {
695
- // 3. Write our additional content
878
+ try {
879
+ const htmlContent = readFileSync(fileToRead);
880
+ res.write(htmlContent);
696
881
  if (htmlPathOld) {
697
882
  res.write("<script>window.__DINOU_USE_OLD_RSC__=true;</script>");
698
883
  }
699
884
  res.write("<script>window.__DINOU_USE_STATIC__=true;</script>");
700
-
701
- // 4. Manually close the response (now we do)
702
885
  res.end();
703
- });
704
-
705
- 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;
706
891
  }
707
892
  }
708
893
 
@@ -739,6 +924,7 @@ app.get(/^\/.*\/?$/, (req, res) => {
739
924
  res,
740
925
  capturedStatus,
741
926
  isDynamic,
927
+ isPathBlocked,
742
928
  );
743
929
 
744
930
  res.setHeader("Content-Type", "text/html");
@@ -968,57 +1154,56 @@ app.post("/____server_function____", async (req, res) => {
968
1154
  return res.status(400).json({ error: "Export is not a function" });
969
1155
  }
970
1156
 
971
- await processLimiter.run(async () => {
972
- const context = getContextForServerFunctionEndpoint(req, res);
1157
+ const context = getContextForServerFunctionEndpoint(req, res);
973
1158
 
974
- let result;
975
- try {
976
- result = await requestStorage.run(context, async () => {
977
- return await fn(...args);
978
- });
979
- } catch (err) {
980
- // 💡 WE INTERCEPT THE REDIRECT
981
- if (err && err.$$type === "dinou-internal-redirect") {
982
- const safeUrl = JSON.stringify(err.url);
983
-
984
- if (!res.headersSent) {
985
- // SCENARIO A: Clean (Content-Type application/json)
986
- res.setHeader("Content-Type", "application/json");
987
- res.setHeader("X-Dinou-Redirect", err.url);
988
- return res.status(200).json({ redirect: err.url });
989
- } else {
990
- // SCENARIO B: Dirty/Active Stream
991
- // Write a custom line-based stream command
992
- res.write(`D:{"type":"redirect","url":${safeUrl}}\n`);
993
-
994
- // ⚠️ IMPORTANT:
995
- // 1. We close the response, since we redirected and there will be no RSC payload.
996
- res.end();
997
-
998
- // 2. WE STOP execution so it doesn't continue to res.json() below.
999
- return;
1000
- }
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;
1001
1184
  }
1002
- throw err; // If it's another error, throw it to the outer catch
1003
1185
  }
1186
+ throw err; // If it's another error, throw it to the outer catch
1187
+ }
1004
1188
 
1005
- if (!res.headersSent) res.setHeader("Content-Type", "text/x-component");
1006
- const manifestPath = path.resolve(
1007
- process.cwd(),
1008
- isWebpack
1009
- ? `${outputFolder}/react-client-manifest.json`
1010
- : `react_client_manifest/react-client-manifest.json`,
1011
- );
1012
- // Verify that the manifest exists to avoid errors
1013
- if (!existsSync(manifestPath)) {
1014
- return res.status(500).json({ error: "Manifest not found" });
1015
- }
1016
- const manifest = isDevelopment
1017
- ? JSON.parse(readFileSync(manifestPath, "utf8"))
1018
- : cachedClientManifest;
1019
- const { pipe } = renderToPipeableStream(result, manifest);
1020
- pipe(res);
1021
- });
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);
1022
1207
  } catch (err) {
1023
1208
  console.error(`Server function error [${req.body?.id}]:`, err);
1024
1209
  // In production, do not send full err.message to avoid leaks
@@ -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.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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou-ejected",
3
- "version": "4.0.16",
3
+ "version": "5.0.1",
4
4
  "main": "index.js",
5
5
  "private": true,
6
6
  "exports": {