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.
- package/CHANGELOG.md +17 -0
- package/README.md +150 -57
- package/dinou/core/babel-esm-loader.js +87 -5
- package/dinou/core/build-static-pages.js +17 -97
- package/dinou/core/generate-static-page.js +21 -3
- package/dinou/core/generate-static-pages.js +30 -41
- package/dinou/core/generate-static-rsc.js +11 -5
- package/dinou/core/generate-static-rscs.js +11 -5
- package/dinou/core/generate-static.js +2 -3
- package/dinou/core/get-jsx.js +2 -1
- package/dinou/core/get-ssg-metadata.js +5 -13
- package/dinou/core/import-module.js +25 -6
- package/dinou/core/redirect.jsx +18 -11
- package/dinou/core/register-loader.mjs +1 -0
- package/dinou/core/render-app-to-html.js +209 -35
- package/dinou/core/render-html.js +144 -26
- package/dinou/core/render-jsx-to-client-jsx.js +6 -7
- package/dinou/core/request-context.js +11 -6
- package/dinou/core/revalidating.js +29 -25
- package/dinou/core/server.js +279 -88
- package/dinou/core/url-resolver.js +3 -0
- package/dinou/esbuild/dev.mjs +31 -2
- package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +0 -40
- package/dinou/index.d.ts +1 -1
- package/dinou/index.mjs +31 -10
- package/dinou/package.json +1 -1
- package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +88 -49
- package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +7 -9
- package/dinou/rollup/rollup.config.js +1 -2
- package/dinou/webpack/helpers/get-webpack-entries.js +0 -40
- package/package.json +3 -3
- package/dinou/core/babel-plugin-register-imports.js +0 -50
- package/dinou/core/get-ssg-jsx-or-jsx.js +0 -14
- package/dinou/core/get-ssg-jsx.js +0 -83
- package/dinou/core/jsx-json.js +0 -55
- package/dinou/core/register-hooks.js +0 -18
package/dinou/core/server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
14
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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 =
|
|
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 } =
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
966
|
-
const context = getContextForServerFunctionEndpoint(req, res);
|
|
1157
|
+
const context = getContextForServerFunctionEndpoint(req, res);
|
|
967
1158
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
package/dinou/esbuild/dev.mjs
CHANGED
|
@@ -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
|
-
|
|
219
|
-
|
|
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?:
|
|
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
|
-
|
|
2
|
-
import
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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;
|