dinou 5.0.1 → 5.0.2

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,19 @@ 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.0.2] - 2026-07-08
9
+
10
+ ### 🛡️ Security & Route Control
11
+ * **Route Validation in RSC Payload Endpoints**: Unified security checks across endpoints. Direct requests to fetch RSC payloads (`/____rsc_payload____/*`) now enforce `validateParams()` and `allowISG()` checks matching the main HTML document loader behavior. This prevents direct payload scraping or scanning of blocked/invalid dynamic route parameters.
12
+
13
+ ### 🐛 Bug Fixes
14
+ * **Sequential Compilation in ISR & ISG**: Resolved a critical race condition that caused disk cache desynchronization and persistent hydration mismatch errors.
15
+ * **Issue**: `Promise.all` compiled the RSC payload and HTML page in parallel. The HTML generator read the `rsc.rsc` file from disk before the new payload finished writing, saving a stale timestamp/state in the static HTML file.
16
+ * **Solution**: Refactored `revalidating.js` and `generating-isg.js` to execute sequentially. The RSC payload is compiled and committed first, ensuring the HTML compiler reads the freshly generated RSC file.
17
+ * **ESM Loader Compatibility for Slot Error Boundaries (`importModule` vs `require`)**: Fixed a server-side crash when using React client hooks inside slot error components (`error.tsx`).
18
+ * **Issue**: The framework used CommonJS `require()` to import the slot error components, bypassing the registered ESM custom loader (`register-loader.mjs`). This caused `"use client"` components to be evaluated as Server Components, failing with `useState is not a function`.
19
+ * **Solution**: Updated `get-jsx.js` and `get-error-jsx.js` to load slot error modules using `await importModule()`, ensuring they are correctly resolved as Client Component References.
20
+
8
21
  ## [5.0.1] - 2026-07-07
9
22
 
10
23
  ### Fixed
@@ -64,6 +64,12 @@ const getRSCPayload = (rscKey, isPrefetch = false) => {
64
64
  payloadUrl = "/____rsc_payload____" + url;
65
65
  }
66
66
 
67
+ const buildId = window.__DINOU_BUILD_ID__;
68
+ if (buildId) {
69
+ payloadUrl += (payloadUrl.includes("?") ? "&" : "?") + "buildId=" + buildId;
70
+ window.__DINOU_BUILD_ID__ = undefined;
71
+ }
72
+
67
73
  // 3. Fetch and Cache Storage
68
74
  const promise = createFromFetch(
69
75
  fetch(payloadUrl).then((res) => {
@@ -41,33 +41,28 @@ function generatingISG(reqPath, isDynamicFromServer) {
41
41
  return; // 👋 We exit without doing anything, the page is dynamic
42
42
  }
43
43
 
44
- // B. Generate HTML and RSC (Same as ISR)
45
- const [pageResult, rscResult] = await Promise.all([
46
- generateStaticPage(reqPath),
47
- generateStaticRSC(reqPath),
48
- ]);
49
-
50
- // C. Transactional Commit
51
- if (pageResult.success && rscResult.success) {
52
- await safeRename(pageResult.tempPath, pageResult.finalPath);
44
+ // B. Generate HTML and RSC sequentially (RSC first, then HTML)
45
+ const rscResult = await generateStaticRSC(reqPath);
46
+ if (!rscResult.success) {
47
+ console.warn(`⚠️ [ISG] RSC generation failed for ${reqPath}. Aborting.`);
48
+ await fs.unlink(rscResult.tempPath).catch(() => {});
49
+ return;
50
+ }
53
51
 
54
- // 2. Rename RSC (.tmp -> .rsc)
55
- // HERE is where the EPERM used to fail
56
- await safeRename(rscResult.tempPath, rscResult.finalPath);
57
- // // Rename .tmp -> Final
58
- // await fs.rename(pageResult.tempPath, pageResult.finalPath);
59
- // await fs.rename(rscResult.tempPath, rscResult.finalPath);
52
+ // Commit the RSC payload immediately so that generateStaticPage can read it
53
+ await safeRename(rscResult.tempPath, rscResult.finalPath);
60
54
 
61
- // Update Manifest
55
+ const pageResult = await generateStaticPage(reqPath);
56
+ if (pageResult.success) {
57
+ await safeRename(pageResult.tempPath, pageResult.finalPath);
62
58
  updateStatus(reqPath, pageResult.status);
63
59
  isDynamicFromServer.value = false;
64
60
  console.log(`✅ [ISG] Successfully promoted ${reqPath} to static.`);
65
61
  } else {
66
- // Cleanup if failed
67
- await fs.unlink(pageResult.tempPath).catch(() => { });
68
- await fs.unlink(rscResult.tempPath).catch(() => { });
69
- console.warn(`⚠️ [ISG] Failed to generate HTML/RSC for ${reqPath}`);
62
+ await fs.unlink(pageResult.tempPath).catch(() => {});
63
+ console.warn(`⚠️ [ISG] HTML generation failed for ${reqPath}. Aborting commit.`);
70
64
  }
65
+
71
66
  } catch (e) {
72
67
  console.error(`[ISG] Critical error promoting ${reqPath}:`, e);
73
68
  } finally {
@@ -140,7 +140,7 @@ async function getErrorJSX(reqPath, query, error, isDevelopment = false) {
140
140
  );
141
141
 
142
142
  if (slotErrorPath) {
143
- const slotErrorModule = require(slotErrorPath);
143
+ const slotErrorModule = await importModule(slotErrorPath);
144
144
  const SlotError = slotErrorModule.default ?? slotErrorModule;
145
145
 
146
146
  const serializedError = {
@@ -196,7 +196,7 @@ async function getJSX(
196
196
  );
197
197
 
198
198
  if (slotErrorPath) {
199
- const slotErrorModule = require(slotErrorPath);
199
+ const slotErrorModule = await importModule(slotErrorPath);
200
200
  const SlotError = slotErrorModule.default ?? slotErrorModule;
201
201
 
202
202
  const serializedError = {
@@ -37,7 +37,9 @@ function revalidating(reqPath, isDynamicFromServer) {
37
37
  path.join(dist2Folder, reqPath, "rsc.rsc"),
38
38
  path.join(dist2Folder, reqPath, "rsc._old.rsc")
39
39
  );
40
- } catch (e) {}
40
+ } catch (e) {
41
+ console.error("[ISR] copyFileSync error:", e);
42
+ }
41
43
 
42
44
  regenerating.add(reqPath);
43
45
 
@@ -55,14 +57,19 @@ function revalidating(reqPath, isDynamicFromServer) {
55
57
  return;
56
58
  }
57
59
 
58
- const [pageResult, rscResult] = await Promise.all([
59
- generateStaticPage(reqPath),
60
- generateStaticRSC(reqPath),
61
- ]);
60
+ const rscResult = await generateStaticRSC(reqPath);
61
+ if (!rscResult.success) {
62
+ console.warn(`⚠️ [ISR] RSC generation failed for ${reqPath}. Aborting.`);
63
+ await fs.unlink(rscResult.tempPath).catch(() => {});
64
+ return;
65
+ }
62
66
 
63
- if (pageResult.success && rscResult.success) {
67
+ // Commit the RSC payload immediately so that generateStaticPage can read it
68
+ await safeRename(rscResult.tempPath, rscResult.finalPath);
69
+
70
+ const pageResult = await generateStaticPage(reqPath);
71
+ if (pageResult.success) {
64
72
  await safeRename(pageResult.tempPath, pageResult.finalPath);
65
- await safeRename(rscResult.tempPath, rscResult.finalPath);
66
73
  updateStatus(reqPath, pageResult.status);
67
74
  isDynamicFromServer.value = false;
68
75
  console.log(
@@ -70,11 +77,9 @@ function revalidating(reqPath, isDynamicFromServer) {
70
77
  );
71
78
  } else {
72
79
  console.warn(
73
- `⚠️ [ISR] Partial failure for ${reqPath}. Aborting commit.`
80
+ `⚠️ [ISR] HTML generation failed for ${reqPath}. Aborting commit.`
74
81
  );
75
-
76
82
  await fs.unlink(pageResult.tempPath).catch(() => {});
77
- await fs.unlink(rscResult.tempPath).catch(() => {});
78
83
  }
79
84
  } catch (e) {
80
85
  console.error(`[ISR] Critical error regenerating ${reqPath}:`, e);
@@ -621,11 +621,28 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
621
621
  // reqPath
622
622
  // );
623
623
  if ((!isDevelopment && !dynamicState.value) || isStatic) {
624
+ let currentGeneratedAt = null;
625
+ try {
626
+ const metadataPath = path.join("dist2", reqPath, "metadata.json");
627
+ if (existsSync(metadataPath)) {
628
+ const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
629
+ currentGeneratedAt = metaObj.generatedAt || null;
630
+ }
631
+ } catch (e) {}
632
+
633
+ const useOld =
634
+ isOld ||
635
+ regenerating.has(reqPath) ||
636
+ (req.query.buildId &&
637
+ currentGeneratedAt &&
638
+ req.query.buildId !== String(currentGeneratedAt));
639
+
624
640
  const payloadPath = path.resolve(
625
641
  "dist2",
626
642
  reqPath.replace(/^\//, ""),
627
- isOld || regenerating.has(reqPath) ? "rsc._old.rsc" : "rsc.rsc",
643
+ useOld ? "rsc._old.rsc" : "rsc.rsc",
628
644
  );
645
+
629
646
  const distDir = path.resolve("dist2");
630
647
 
631
648
  if (!payloadPath.startsWith(distDir)) {
@@ -633,6 +650,7 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
633
650
  }
634
651
  if (existsSync(payloadPath)) {
635
652
  res.setHeader("Content-Type", "application/octet-stream");
653
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
636
654
  try {
637
655
  const buffer = readFileSync(payloadPath);
638
656
  return res.send(buffer);
@@ -642,6 +660,103 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
642
660
  }
643
661
  }
644
662
  }
663
+ let isPathBlocked = false;
664
+ const reqSegments = reqPath.split("/").filter(Boolean);
665
+ const srcFolder = path.resolve(process.cwd(), "src");
666
+ const [pagePath, dynamicParams] = getFilePathAndDynamicParams(
667
+ reqSegments,
668
+ req.query,
669
+ srcFolder,
670
+ );
671
+
672
+ if (pagePath) {
673
+ let cachedConfig = pageFunctionsConfigCache.get(pagePath);
674
+ if (!cachedConfig) {
675
+ const pageFolder = path.dirname(pagePath);
676
+ const [pageFunctionsPath] = getFilePathAndDynamicParams(
677
+ reqSegments,
678
+ req.query,
679
+ pageFolder,
680
+ "page_functions",
681
+ true,
682
+ true,
683
+ undefined,
684
+ reqSegments.length,
685
+ );
686
+
687
+ if (pageFunctionsPath) {
688
+ const pageFunctionsModule = await importModule(pageFunctionsPath);
689
+ const allowISGValue = pageFunctionsModule.allowISG
690
+ ? await pageFunctionsModule.allowISG()
691
+ : true;
692
+
693
+ let staticPathsSet = null;
694
+ if (pageFunctionsModule.getStaticPaths) {
695
+ const paths = await pageFunctionsModule.getStaticPaths();
696
+ staticPathsSet = new Set(
697
+ (paths || []).map((pathObj) => {
698
+ const sortedEntries = Object.entries(pathObj).sort((a, b) =>
699
+ a[0].localeCompare(b[0])
700
+ );
701
+ return JSON.stringify(sortedEntries);
702
+ })
703
+ );
704
+ }
705
+
706
+ cachedConfig = {
707
+ allowISG: allowISGValue,
708
+ staticPathsSet,
709
+ validateParams: pageFunctionsModule.validateParams || null,
710
+ };
711
+ } else {
712
+ cachedConfig = {
713
+ allowISG: true,
714
+ staticPathsSet: null,
715
+ validateParams: null,
716
+ };
717
+ }
718
+
719
+ if (!isDevelopment) {
720
+ pageFunctionsConfigCache.set(pagePath, cachedConfig);
721
+ }
722
+ }
723
+
724
+ const { allowISG: allowISGValue, staticPathsSet, validateParams: validateParamsFn } = cachedConfig;
725
+ const hasParams = Object.keys(dynamicParams || {}).length > 0;
726
+ if (hasParams) {
727
+ if (validateParamsFn) {
728
+ const isValid = await validateParamsFn(dynamicParams);
729
+ if (!isValid) {
730
+ isPathBlocked = true;
731
+ }
732
+ }
733
+
734
+ if (!isPathBlocked && allowISGValue === false) {
735
+ let isPathAllowed = false;
736
+ if (staticPathsSet) {
737
+ const sortedQueryEntries = Object.entries(dynamicParams)
738
+ .sort((a, b) => a[0].localeCompare(b[0]))
739
+ .map(([k, v]) => {
740
+ if (Array.isArray(v)) return [k, v.join(",")];
741
+ return [k, String(v)];
742
+ });
743
+ const serializedQuery = JSON.stringify(sortedQueryEntries);
744
+ isPathAllowed = staticPathsSet.has(serializedQuery);
745
+ }
746
+ if (!isPathAllowed) {
747
+ if (isDevelopment) {
748
+ isPathBlocked = true;
749
+ } else {
750
+ const htmlPath = path.join("dist2", reqPath, "index.html");
751
+ if (!existsSync(htmlPath)) {
752
+ isPathBlocked = true;
753
+ }
754
+ }
755
+ }
756
+ }
757
+ }
758
+ }
759
+
645
760
  const context = getContext(req, res);
646
761
  const isNotFound = {};
647
762
  await requestStorage.run(context, async () => {
@@ -650,6 +765,7 @@ async function serveRSCPayload(req, res, isOld = false, isStatic = false) {
650
765
  { ...req.query },
651
766
  isNotFound,
652
767
  isDevelopment,
768
+ isPathBlocked,
653
769
  );
654
770
 
655
771
  if (isNotFound.value) {
@@ -868,6 +984,7 @@ app.get(/^\/.*\/?$/, async (req, res) => {
868
984
 
869
985
  if (existsSync(fileToRead) && !dynamicState.value) {
870
986
  res.setHeader("Content-Type", "text/html");
987
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
871
988
  const meta = getStatus[reqPath];
872
989
 
873
990
  if (meta && meta.status) {
@@ -876,13 +993,26 @@ app.get(/^\/.*\/?$/, async (req, res) => {
876
993
  res.statusCode = 200; // Default
877
994
  }
878
995
  try {
879
- const htmlContent = readFileSync(fileToRead);
880
- res.write(htmlContent);
996
+ let htmlContent = readFileSync(fileToRead, "utf8");
997
+ let buildId = "";
998
+ try {
999
+ const metadataPath = path.join("dist2", reqPath, "metadata.json");
1000
+ if (existsSync(metadataPath)) {
1001
+ const metaObj = JSON.parse(readFileSync(metadataPath, "utf8"));
1002
+ buildId = metaObj.generatedAt || "";
1003
+ }
1004
+ } catch (e) {}
1005
+
1006
+ let scripts = `<script>window.__DINOU_USE_STATIC__=true;</script>`;
881
1007
  if (htmlPathOld) {
882
- res.write("<script>window.__DINOU_USE_OLD_RSC__=true;</script>");
1008
+ scripts += `<script>window.__DINOU_USE_OLD_RSC__=true;</script>`;
883
1009
  }
884
- res.write("<script>window.__DINOU_USE_STATIC__=true;</script>");
885
- res.end();
1010
+ if (buildId) {
1011
+ scripts += `<script>window.__DINOU_BUILD_ID__="${buildId}";</script>`;
1012
+ }
1013
+
1014
+ htmlContent = htmlContent.replace("</head>", `${scripts}</head>`);
1015
+ res.send(htmlContent);
886
1016
  } catch (err) {
887
1017
  console.error("Error reading HTML file:", err);
888
1018
  if (!res.headersSent) res.status(500).send("Server Error");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou-ejected",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
4
4
  "main": "index.js",
5
5
  "private": true,
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
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": {