dinou 5.0.3 → 5.1.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 +13 -0
- package/README.md +67 -4
- package/dinou/core/build-static-pages.js +120 -4
- package/dinou/core/cache-revalidate.js +145 -0
- package/dinou/core/generate-static-page.js +2 -1
- package/dinou/core/generate-static-pages.js +1 -0
- package/dinou/core/generate-static-rsc.js +1 -2
- package/dinou/core/server.js +3 -3
- package/dinou/package.json +6 -1
- package/dinou/server.d.ts +12 -0
- package/dinou/server.js +10 -0
- package/dinou/server.mjs +9 -0
- package/package.json +1 -1
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.1.0] - 2026-07-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **On-Demand Revalidation API**: Added `revalidatePath` and `revalidateTag` exports to the new `"dinou/server"` server-only entry point.
|
|
12
|
+
- **`revalidatePath(path)`**: Triggers the purge and background regeneration of static files for the targeted route. Fully supports relative path navigation inputs (e.g. `./` or `detalles`), resolved dynamically at runtime using the active request context and the `referer` header.
|
|
13
|
+
- **`revalidateTag(tag)`**: Scans statically generated outputs to locate and regenerate all routes associated with the matching cache tag.
|
|
14
|
+
- **Route Caching Tags (`getCacheTags`)**: Integrated support for defining a custom `getCacheTags(params)` function inside route-level `page_functions` files. Registered tags are saved into the route's `metadata.json` during compile time and subsequent on-demand builds for tag-based invalidation queries.
|
|
15
|
+
- **Lazy Load Module Boundaries**: Implemented dynamic CJS `require` and ESM `import()` layers inside the `"dinou/server"` entry point. This safely halts recursive import chains, preventing client-side bundlers and SSR rendering subprocesses from evaluating server-exclusive Node.js runtime code.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Slot Error Boundary Recovery during Static Site Generation (SSG)**: Implemented isolated try-catch evaluation blocks and local `error.jsx` fallback resolutions for layout slots inside `build-static-pages.js`. This matches the runtime router's slot containment behavior, preventing slot-level crashes from bubbling up and aborting compile-time builds or revalidation processes.
|
|
19
|
+
- **HTTP Status Retrieval Bug in Static Serving (`getStatus`)**: Corrected the invocation of the status manifest retrieval logic in `server.js` (replacing the faulty `getStatus[reqPath]` property lookup with the correct `getStatus(reqPath)` function call). This ensures the server serves pre-rendered static pages with their recorded status codes (e.g. `500` for slot/page errors) instead of falling back to a `200 OK` default.
|
|
20
|
+
|
|
8
21
|
## [5.0.3] - 2026-07-09
|
|
9
22
|
|
|
10
23
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# **Dinou**
|
|
2
2
|
|
|
3
|
-
[](https://dinou.dev)
|
|
3
|
+
[](https://dinou.dev) [](https://www.npmjs.com/package/dinou)
|
|
4
4
|
|
|
5
5
|
### **Lightweight-Ejectable Full-Stack React 19 Framework**
|
|
6
6
|
|
|
@@ -53,6 +53,7 @@ Support for React Server Components (RSC), Server Functions, Server-Side Renderi
|
|
|
53
53
|
- [4. `dynamic` (Force SSR)](#4-dynamic-force-ssr)
|
|
54
54
|
- [5. `validateParams` (Route Parameter Validation)](#5-validateparams-route-parameter-validation)
|
|
55
55
|
- [6. `allowISG` (Control On-Demand Generation)](#6-allowisg-control-on-demand-generation)
|
|
56
|
+
- [7. `getCacheTags` (Route Caching Tags)](#7-getcachetags-route-caching-tags)
|
|
56
57
|
- [⚡ React Compiler & Automatic Optimizations](#-react-compiler--automatic-optimizations)
|
|
57
58
|
- [Integration Strategy](#integration-strategy)
|
|
58
59
|
- [Code Example](#code-example)
|
|
@@ -60,7 +61,7 @@ Support for React Server Components (RSC), Server Functions, Server-Side Renderi
|
|
|
60
61
|
- [1. Components (`dinou`)](#1-components-dinou)
|
|
61
62
|
- [2. Utilities (`dinou`)](#2-utilities-dinou)
|
|
62
63
|
- [3. Client Hooks (`dinou` - Client Components Only)](#3-client-hooks-dinou---client-components-only)
|
|
63
|
-
- [4. Server-Only Utilities (`dinou`)](#4-server-only-utilities
|
|
64
|
+
- [4. Server-Only Utilities (`dinou` & `dinou/server`)](#4-server-only-utilities)
|
|
64
65
|
- [5. Page Configuration (`page_functions.ts`)](#5-page-configuration-page_functionsts)
|
|
65
66
|
- [6. File Conventions Cheatsheet](#6-file-conventions-cheatsheet)
|
|
66
67
|
- [🎨 Favicons](#-favicons)
|
|
@@ -912,6 +913,21 @@ export function allowISG() {
|
|
|
912
913
|
}
|
|
913
914
|
```
|
|
914
915
|
|
|
916
|
+
### 7. `getCacheTags` (Route Caching Tags)
|
|
917
|
+
|
|
918
|
+
Associates invalidation tags with the pre-rendered or dynamic route for tag-based on-demand revalidation.
|
|
919
|
+
|
|
920
|
+
- **Arguments:** `params` (The dynamic route parameters).
|
|
921
|
+
- **Returns:** `string[]` or `Promise<string[]>` (an array of cache tags).
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
// src/posts/[id]/page_functions.ts
|
|
925
|
+
export async function getCacheTags(params) {
|
|
926
|
+
// Tag this page with a generic collection tag and a specific resource tag
|
|
927
|
+
return ["posts", `post-${params.id}`];
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
915
931
|
## ⚡ React Compiler & Automatic Optimizations
|
|
916
932
|
|
|
917
933
|
Dinou integrates the **React Compiler** (React 19+) out of the box. It analyzes your code and automatically applies fine-grained memoization to values and functions.
|
|
@@ -1105,9 +1121,9 @@ export default function LoadingBar() {
|
|
|
1105
1121
|
|
|
1106
1122
|
---
|
|
1107
1123
|
|
|
1108
|
-
### 4. Server-Only Utilities
|
|
1124
|
+
### 4. Server-Only Utilities
|
|
1109
1125
|
|
|
1110
|
-
#### `getContext()`
|
|
1126
|
+
#### `getContext()` (from `"dinou"`)
|
|
1111
1127
|
|
|
1112
1128
|
Retrieves the request/response context. Available **only** inside server-side execution threads (Server Components, `page_functions` like `getProps`, and Server Functions).
|
|
1113
1129
|
|
|
@@ -1115,6 +1131,40 @@ Retrieves the request/response context. Available **only** inside server-side ex
|
|
|
1115
1131
|
- **req:** `headers`, `cookies`, `query`, `path`, `method`.
|
|
1116
1132
|
- **res:** `status()`, `setHeader()`, `redirect()`, `cookie()`, `clearCookie()`.
|
|
1117
1133
|
|
|
1134
|
+
#### On-Demand Revalidation (from `"dinou/server"`)
|
|
1135
|
+
|
|
1136
|
+
These server-only utilities must be imported from the `"dinou/server"` entry point.
|
|
1137
|
+
|
|
1138
|
+
##### `revalidatePath(path)`
|
|
1139
|
+
|
|
1140
|
+
Triggers background regeneration of static files for the targeted route path. Supports relative paths (e.g. `./` or `detalles`), resolved relative to the request referer context at runtime.
|
|
1141
|
+
|
|
1142
|
+
- **Arguments:** `path` (a string representing the route to revalidate).
|
|
1143
|
+
- **Returns:** `void`.
|
|
1144
|
+
|
|
1145
|
+
```javascript
|
|
1146
|
+
import { revalidatePath } from "dinou/server";
|
|
1147
|
+
|
|
1148
|
+
export async function action() {
|
|
1149
|
+
revalidatePath("/blog/posts");
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
##### `revalidateTag(tag)`
|
|
1154
|
+
|
|
1155
|
+
Triggers background regeneration of all routes registered with the matching cache tag.
|
|
1156
|
+
|
|
1157
|
+
- **Arguments:** `tag` (a string representing the tag to invalidate).
|
|
1158
|
+
- **Returns:** `void`.
|
|
1159
|
+
|
|
1160
|
+
```javascript
|
|
1161
|
+
import { revalidateTag } from "dinou/server";
|
|
1162
|
+
|
|
1163
|
+
export async function action() {
|
|
1164
|
+
revalidateTag("posts");
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1118
1168
|
#### ⚠️ Security Warning: `getContext` in Client Components
|
|
1119
1169
|
|
|
1120
1170
|
While `getContext()` technically works during the Server-Side Rendering (SSR) phase of Client Components, **using it directly inside a Client Component is strongly discouraged**.
|
|
@@ -1234,6 +1284,19 @@ export function allowISG() {
|
|
|
1234
1284
|
}
|
|
1235
1285
|
```
|
|
1236
1286
|
|
|
1287
|
+
#### `getCacheTags(params)`
|
|
1288
|
+
|
|
1289
|
+
Defines cache tags associated with the pre-rendered page for tag-based invalidation.
|
|
1290
|
+
|
|
1291
|
+
- **Receives:** Resolved `params` object.
|
|
1292
|
+
- **Returns:** `string[]` or `Promise<string[]>` (an array of cache tag strings).
|
|
1293
|
+
|
|
1294
|
+
```typescript
|
|
1295
|
+
export function getCacheTags(params) {
|
|
1296
|
+
return ["posts", `post-${params.id}`];
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1237
1300
|
---
|
|
1238
1301
|
|
|
1239
1302
|
### 6. File Conventions Cheatsheet
|
|
@@ -824,6 +824,7 @@ async function buildStaticPages() {
|
|
|
824
824
|
};
|
|
825
825
|
|
|
826
826
|
let revalidate;
|
|
827
|
+
let cacheTags = [];
|
|
827
828
|
const jsx = await requestStorage.run(mockContext, async () => {
|
|
828
829
|
const [pagePath] = getFilePathAndDynamicParams(
|
|
829
830
|
segments,
|
|
@@ -857,6 +858,13 @@ async function buildStaticPages() {
|
|
|
857
858
|
const pageFunctionsModule = await importModule(pageFunctionsPath);
|
|
858
859
|
const getProps = pageFunctionsModule.getProps;
|
|
859
860
|
revalidate = pageFunctionsModule.revalidate;
|
|
861
|
+
if (pageFunctionsModule.getCacheTags) {
|
|
862
|
+
try {
|
|
863
|
+
cacheTags = await pageFunctionsModule.getCacheTags(params);
|
|
864
|
+
} catch (e) {
|
|
865
|
+
console.error("Error running getCacheTags in buildStaticPages:", e);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
860
868
|
pageFunctionsProps = await getProps?.(params);
|
|
861
869
|
props = { ...props, ...(pageFunctionsProps?.page ?? {}) };
|
|
862
870
|
}
|
|
@@ -896,11 +904,60 @@ async function buildStaticPages() {
|
|
|
896
904
|
const updatedSlots = {};
|
|
897
905
|
for (const [slotName, slotElement] of Object.entries(slots)) {
|
|
898
906
|
const alreadyFoundPath = slotElement.props?.__modulePath;
|
|
899
|
-
|
|
900
|
-
updatedSlots[slotName] = {
|
|
907
|
+
const slotElementWithModule = {
|
|
901
908
|
...slotElement,
|
|
902
909
|
__modulePath: alreadyFoundPath ?? null,
|
|
903
910
|
};
|
|
911
|
+
|
|
912
|
+
let updatedSlotElement;
|
|
913
|
+
try {
|
|
914
|
+
await asyncRenderJSXToClientJSX(slotElementWithModule);
|
|
915
|
+
updatedSlotElement = slotElementWithModule;
|
|
916
|
+
} catch (e) {
|
|
917
|
+
const slotFilePath = alreadyFoundPath;
|
|
918
|
+
if (slotFilePath) {
|
|
919
|
+
const realSlotFolder = path.dirname(slotFilePath);
|
|
920
|
+
const [slotErrorPath, slotErrorParams] =
|
|
921
|
+
getFilePathAndDynamicParams(
|
|
922
|
+
segments,
|
|
923
|
+
{},
|
|
924
|
+
realSlotFolder,
|
|
925
|
+
"error",
|
|
926
|
+
true,
|
|
927
|
+
true,
|
|
928
|
+
undefined,
|
|
929
|
+
segments.length,
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
if (slotErrorPath) {
|
|
933
|
+
const slotErrorModule = await importModule(slotErrorPath);
|
|
934
|
+
const SlotError = slotErrorModule.default ?? slotErrorModule;
|
|
935
|
+
|
|
936
|
+
const serializedError = {
|
|
937
|
+
message: e.message || "Unknown Error",
|
|
938
|
+
name: e.name,
|
|
939
|
+
stack: process.env.NODE_ENV !== "production" ? e.stack : undefined,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
updatedSlotElement = React.createElement(SlotError, {
|
|
943
|
+
params: slotErrorParams,
|
|
944
|
+
key: slotName,
|
|
945
|
+
error: serializedError,
|
|
946
|
+
});
|
|
947
|
+
} else {
|
|
948
|
+
console.warn(
|
|
949
|
+
`[Dinou Build] Slot @${slotName} failed and does not have error.tsx`,
|
|
950
|
+
);
|
|
951
|
+
updatedSlotElement = null;
|
|
952
|
+
}
|
|
953
|
+
} else {
|
|
954
|
+
console.error(
|
|
955
|
+
`[Dinou Build] Slot @${slotName} failed and has no module path`,
|
|
956
|
+
);
|
|
957
|
+
updatedSlotElement = null;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
updatedSlots[slotName] = updatedSlotElement;
|
|
904
961
|
}
|
|
905
962
|
let props = {
|
|
906
963
|
params: dParams,
|
|
@@ -943,6 +1000,7 @@ async function buildStaticPages() {
|
|
|
943
1000
|
staticMetadata.set(reqPath, {
|
|
944
1001
|
revalidate: revalidate?.(),
|
|
945
1002
|
effects: sideEffects,
|
|
1003
|
+
tags: cacheTags,
|
|
946
1004
|
});
|
|
947
1005
|
console.log(`Registered static page at ${reqPath}`);
|
|
948
1006
|
} catch (err) {
|
|
@@ -1096,6 +1154,7 @@ async function buildStaticPage(reqPath, isDynamic = null) {
|
|
|
1096
1154
|
};
|
|
1097
1155
|
|
|
1098
1156
|
let revalidate;
|
|
1157
|
+
let cacheTags = [];
|
|
1099
1158
|
const jsx = await requestStorage.run(mockContext, async () => {
|
|
1100
1159
|
const [pagePath, dParams] = getFilePathAndDynamicParams(
|
|
1101
1160
|
segments,
|
|
@@ -1132,6 +1191,13 @@ async function buildStaticPage(reqPath, isDynamic = null) {
|
|
|
1132
1191
|
if (isDynamic && (isDynamic.value = pageFunctionsModule.dynamic?.()))
|
|
1133
1192
|
return;
|
|
1134
1193
|
revalidate = pageFunctionsModule.revalidate;
|
|
1194
|
+
if (pageFunctionsModule.getCacheTags) {
|
|
1195
|
+
try {
|
|
1196
|
+
cacheTags = await pageFunctionsModule.getCacheTags(dParams);
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
console.error("Error running getCacheTags:", e);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1135
1201
|
pageFunctionsProps = await getProps?.(dParams);
|
|
1136
1202
|
props = { ...props, ...(pageFunctionsProps?.page ?? {}) };
|
|
1137
1203
|
}
|
|
@@ -1169,11 +1235,60 @@ async function buildStaticPage(reqPath, isDynamic = null) {
|
|
|
1169
1235
|
const updatedSlots = {};
|
|
1170
1236
|
for (const [slotName, slotElement] of Object.entries(slots)) {
|
|
1171
1237
|
const alreadyFoundPath = slotElement.props?.__modulePath;
|
|
1172
|
-
|
|
1173
|
-
updatedSlots[slotName] = {
|
|
1238
|
+
const slotElementWithModule = {
|
|
1174
1239
|
...slotElement,
|
|
1175
1240
|
__modulePath: alreadyFoundPath ?? null,
|
|
1176
1241
|
};
|
|
1242
|
+
|
|
1243
|
+
let updatedSlotElement;
|
|
1244
|
+
try {
|
|
1245
|
+
await asyncRenderJSXToClientJSX(slotElementWithModule);
|
|
1246
|
+
updatedSlotElement = slotElementWithModule;
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
const slotFilePath = alreadyFoundPath;
|
|
1249
|
+
if (slotFilePath) {
|
|
1250
|
+
const realSlotFolder = path.dirname(slotFilePath);
|
|
1251
|
+
const [slotErrorPath, slotErrorParams] =
|
|
1252
|
+
getFilePathAndDynamicParams(
|
|
1253
|
+
segments,
|
|
1254
|
+
{},
|
|
1255
|
+
realSlotFolder,
|
|
1256
|
+
"error",
|
|
1257
|
+
true, // withExtension
|
|
1258
|
+
true, // finalDestination
|
|
1259
|
+
undefined, // lastFound
|
|
1260
|
+
segments.length,
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
if (slotErrorPath) {
|
|
1264
|
+
const slotErrorModule = await importModule(slotErrorPath);
|
|
1265
|
+
const SlotError = slotErrorModule.default ?? slotErrorModule;
|
|
1266
|
+
|
|
1267
|
+
const serializedError = {
|
|
1268
|
+
message: e.message || "Unknown Error",
|
|
1269
|
+
name: e.name,
|
|
1270
|
+
stack: process.env.NODE_ENV !== "production" ? e.stack : undefined,
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
updatedSlotElement = React.createElement(SlotError, {
|
|
1274
|
+
params: slotErrorParams,
|
|
1275
|
+
key: slotName,
|
|
1276
|
+
error: serializedError,
|
|
1277
|
+
});
|
|
1278
|
+
} else {
|
|
1279
|
+
console.warn(
|
|
1280
|
+
`[Dinou Build] Slot @${slotName} failed and does not have error.tsx`,
|
|
1281
|
+
);
|
|
1282
|
+
updatedSlotElement = null;
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
console.error(
|
|
1286
|
+
`[Dinou Build] Slot @${slotName} failed and has no module path`,
|
|
1287
|
+
);
|
|
1288
|
+
updatedSlotElement = null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
updatedSlots[slotName] = updatedSlotElement;
|
|
1177
1292
|
}
|
|
1178
1293
|
let layoutProps = {
|
|
1179
1294
|
params: dParams,
|
|
@@ -1221,6 +1336,7 @@ async function buildStaticPage(reqPath, isDynamic = null) {
|
|
|
1221
1336
|
staticMetadata.set(reqPath, {
|
|
1222
1337
|
revalidate: revalidate?.(),
|
|
1223
1338
|
effects: sideEffects,
|
|
1339
|
+
tags: cacheTags,
|
|
1224
1340
|
});
|
|
1225
1341
|
console.warn(`Registered rebuilt static page at ${reqPath}`);
|
|
1226
1342
|
} catch (error) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs").promises;
|
|
3
|
+
const { existsSync, copyFileSync } = require("fs");
|
|
4
|
+
const generateStaticPage = require("./generate-static-page");
|
|
5
|
+
const { buildStaticPage } = require("./build-static-pages");
|
|
6
|
+
const generateStaticRSC = require("./generate-static-rsc");
|
|
7
|
+
const { safeRename } = require("./safe-rename");
|
|
8
|
+
const { updateStatus } = require("./status-manifest");
|
|
9
|
+
|
|
10
|
+
const { getContext } = require("./request-context");
|
|
11
|
+
const { resolveRelativeUrl } = require("./url-resolver");
|
|
12
|
+
|
|
13
|
+
async function walkMetadataFiles(dir, fileList = []) {
|
|
14
|
+
try {
|
|
15
|
+
const files = await fs.readdir(dir);
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const filePath = path.join(dir, file);
|
|
18
|
+
const stat = await fs.stat(filePath);
|
|
19
|
+
if (stat.isDirectory()) {
|
|
20
|
+
await walkMetadataFiles(filePath, fileList);
|
|
21
|
+
} else if (file === "metadata.json") {
|
|
22
|
+
fileList.push(filePath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// Ignore read errors for individual folders/files
|
|
27
|
+
}
|
|
28
|
+
return fileList;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function revalidatePath(reqPath) {
|
|
32
|
+
let targetPath = reqPath;
|
|
33
|
+
|
|
34
|
+
if (targetPath && !targetPath.startsWith("/") && !targetPath.includes("://")) {
|
|
35
|
+
const ctx = getContext();
|
|
36
|
+
let currentPathname = "/";
|
|
37
|
+
if (ctx && ctx.req) {
|
|
38
|
+
const referer = ctx.req.headers?.referer;
|
|
39
|
+
if (referer) {
|
|
40
|
+
try {
|
|
41
|
+
currentPathname = new URL(referer).pathname;
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
} else {
|
|
44
|
+
currentPathname = ctx.req.path || "/";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
targetPath = resolveRelativeUrl(targetPath, currentPathname);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cleanPath = targetPath;
|
|
51
|
+
if (!cleanPath.startsWith("/")) {
|
|
52
|
+
cleanPath = "/" + cleanPath;
|
|
53
|
+
}
|
|
54
|
+
if (cleanPath !== "/" && cleanPath.endsWith("/")) {
|
|
55
|
+
cleanPath = cleanPath.slice(0, -1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dist2Folder = path.resolve(process.cwd(), "dist2");
|
|
59
|
+
const reqPathWithSlash = cleanPath.endsWith("/") ? cleanPath : cleanPath + "/";
|
|
60
|
+
|
|
61
|
+
// Check if there is an existing page to copy to _old
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(path.join(dist2Folder, reqPathWithSlash, "index.html"))) {
|
|
64
|
+
copyFileSync(
|
|
65
|
+
path.join(dist2Folder, reqPathWithSlash, "index.html"),
|
|
66
|
+
path.join(dist2Folder, reqPathWithSlash, "index._old.html")
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(path.join(dist2Folder, reqPathWithSlash, "rsc.rsc"))) {
|
|
70
|
+
copyFileSync(
|
|
71
|
+
path.join(dist2Folder, reqPathWithSlash, "rsc.rsc"),
|
|
72
|
+
path.join(dist2Folder, reqPathWithSlash, "rsc._old.rsc")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Ignore copy errors
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`[Revalidate] Starting on-demand revalidation for ${cleanPath}...`);
|
|
80
|
+
try {
|
|
81
|
+
const isDynamic = {};
|
|
82
|
+
await buildStaticPage(cleanPath, isDynamic);
|
|
83
|
+
if (isDynamic.value) {
|
|
84
|
+
console.log(`[Revalidate] Bailout detected for ${cleanPath}. Switching to dynamic (skipping static write).`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rscResult = await generateStaticRSC(cleanPath);
|
|
89
|
+
if (!rscResult.success) {
|
|
90
|
+
console.warn(`⚠️ [Revalidate] RSC generation failed for ${cleanPath}.`);
|
|
91
|
+
if (rscResult.tempPath && existsSync(rscResult.tempPath)) {
|
|
92
|
+
await fs.unlink(rscResult.tempPath).catch(() => {});
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await safeRename(rscResult.tempPath, rscResult.finalPath);
|
|
98
|
+
|
|
99
|
+
const pageResult = await generateStaticPage(cleanPath);
|
|
100
|
+
if (pageResult.success) {
|
|
101
|
+
await safeRename(pageResult.tempPath, pageResult.finalPath);
|
|
102
|
+
updateStatus(cleanPath, pageResult.status);
|
|
103
|
+
console.log(`✅ [Revalidate] Successfully revalidated ${cleanPath} (Status: ${pageResult.status})`);
|
|
104
|
+
} else {
|
|
105
|
+
console.warn(`⚠️ [Revalidate] HTML generation failed for ${cleanPath}.`);
|
|
106
|
+
if (pageResult.tempPath && existsSync(pageResult.tempPath)) {
|
|
107
|
+
await fs.unlink(pageResult.tempPath).catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error(`[Revalidate] Critical error revalidating ${cleanPath}:`, e);
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function revalidateTag(tag) {
|
|
117
|
+
console.log(`[Revalidate] Starting on-demand revalidation for tag: "${tag}"...`);
|
|
118
|
+
const dist2Folder = path.resolve(process.cwd(), "dist2");
|
|
119
|
+
if (!existsSync(dist2Folder)) return;
|
|
120
|
+
|
|
121
|
+
const metadataFiles = await walkMetadataFiles(dist2Folder);
|
|
122
|
+
const revalidatePromises = [];
|
|
123
|
+
|
|
124
|
+
for (const fileOfMeta of metadataFiles) {
|
|
125
|
+
try {
|
|
126
|
+
const content = await fs.readFile(fileOfMeta, "utf8");
|
|
127
|
+
const metadata = JSON.parse(content);
|
|
128
|
+
if (metadata && Array.isArray(metadata.tags) && metadata.tags.includes(tag)) {
|
|
129
|
+
// Calculate the request path
|
|
130
|
+
const relative = path.relative(dist2Folder, path.dirname(fileOfMeta));
|
|
131
|
+
const reqPath = "/" + relative.replace(/\\/g, "/");
|
|
132
|
+
revalidatePromises.push(revalidatePath(reqPath));
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`[Revalidate] Error reading tags from ${fileOfMeta}:`, err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await Promise.all(revalidatePromises);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
revalidatePath,
|
|
144
|
+
revalidateTag,
|
|
145
|
+
};
|
|
@@ -10,7 +10,7 @@ const OUT_DIR = path.resolve("dist2");
|
|
|
10
10
|
async function generateStaticPage(reqPath) {
|
|
11
11
|
const finalReqPath = reqPath.endsWith("/") ? reqPath : reqPath + "/";
|
|
12
12
|
const htmlPath = path.join(OUT_DIR, finalReqPath, "index.html");
|
|
13
|
-
const tempHtmlPath = path.join(OUT_DIR, finalReqPath,
|
|
13
|
+
const tempHtmlPath = path.join(OUT_DIR, finalReqPath, `index.html.${Date.now()}-${Math.random()}.tmp`);
|
|
14
14
|
|
|
15
15
|
const query = {};
|
|
16
16
|
const paramsString = JSON.stringify(query);
|
|
@@ -94,6 +94,7 @@ async function generateStaticPage(reqPath) {
|
|
|
94
94
|
revalidate: metadata.revalidate,
|
|
95
95
|
generatedAt: Date.now(),
|
|
96
96
|
effects: metadata.effects,
|
|
97
|
+
tags: metadata.tags || [],
|
|
97
98
|
}, null, 2),
|
|
98
99
|
"utf8"
|
|
99
100
|
);
|
|
@@ -15,9 +15,8 @@ const OUT_DIR = path.resolve("dist2");
|
|
|
15
15
|
async function generateStaticRSC(reqPath) {
|
|
16
16
|
const finalReqPath = reqPath.endsWith("/") ? reqPath : reqPath + "/";
|
|
17
17
|
|
|
18
|
-
// 1. PATHS: Final and Temporary
|
|
19
18
|
const payloadPath = path.join(OUT_DIR, finalReqPath, "rsc.rsc");
|
|
20
|
-
const tempPayloadPath =
|
|
19
|
+
const tempPayloadPath = path.join(OUT_DIR, finalReqPath, `rsc.rsc.${Date.now()}-${Math.random()}.tmp`);
|
|
21
20
|
|
|
22
21
|
// 👇 2. MOCK RES (Complete and Robust Version)
|
|
23
22
|
const mockRes = {
|
package/dinou/core/server.js
CHANGED
|
@@ -1026,10 +1026,10 @@ app.get(/^\/.*\/?$/, async (req, res) => {
|
|
|
1026
1026
|
if (existsSync(fileToRead) && !dynamicState.value) {
|
|
1027
1027
|
res.setHeader("Content-Type", "text/html");
|
|
1028
1028
|
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
1029
|
-
const
|
|
1029
|
+
const status = getStatus(reqPath);
|
|
1030
1030
|
|
|
1031
|
-
if (
|
|
1032
|
-
res.statusCode =
|
|
1031
|
+
if (status) {
|
|
1032
|
+
res.statusCode = status;
|
|
1033
1033
|
} else {
|
|
1034
1034
|
res.statusCode = 200; // Default
|
|
1035
1035
|
}
|
package/dinou/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dinou-ejected",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"private": true,
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"import": "./index.mjs",
|
|
9
9
|
"require": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"./server": {
|
|
12
|
+
"types": "./server.d.ts",
|
|
13
|
+
"import": "./server.mjs",
|
|
14
|
+
"require": "./server.js"
|
|
10
15
|
}
|
|
11
16
|
},
|
|
12
17
|
"types": "./index.d.ts",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revalidates the cache for a specific route path.
|
|
3
|
+
* * It deletes the old cached HTML and RSC files and rebuilds them synchronously.
|
|
4
|
+
* @param path The path of the route to revalidate (e.g. "/blog" or "/blog/12").
|
|
5
|
+
*/
|
|
6
|
+
export declare function revalidatePath(path: string): Promise<void>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Revalidates all routes that are associated with the specified cache tag.
|
|
10
|
+
* @param tag The tag string to revalidate (e.g. "blog-posts").
|
|
11
|
+
*/
|
|
12
|
+
export declare function revalidateTag(tag: string): Promise<void>;
|
package/dinou/server.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
revalidatePath: async function (path) {
|
|
3
|
+
const { revalidatePath: fn } = require("./core/cache-revalidate.js");
|
|
4
|
+
return fn(path);
|
|
5
|
+
},
|
|
6
|
+
revalidateTag: async function (tag) {
|
|
7
|
+
const { revalidateTag: fn } = require("./core/cache-revalidate.js");
|
|
8
|
+
return fn(tag);
|
|
9
|
+
},
|
|
10
|
+
};
|
package/dinou/server.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function revalidatePath(path) {
|
|
2
|
+
const { revalidatePath: fn } = await import("./core/cache-revalidate.js");
|
|
3
|
+
return fn(path);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export async function revalidateTag(tag) {
|
|
7
|
+
const { revalidateTag: fn } = await import("./core/cache-revalidate.js");
|
|
8
|
+
return fn(tag);
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dinou",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.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": {
|