dinou 3.0.5 → 3.0.6

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,12 @@ 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
+ ## [3.0.6]
9
+
10
+ ## Security
11
+
12
+ - Fix: Use fixes from React versions 19.2.3 and improve security of server function endpoint.
13
+
8
14
  ## [3.0.5]
9
15
 
10
16
  ## Security
@@ -0,0 +1,40 @@
1
+ const parser = require("@babel/parser");
2
+ const traverse = require("@babel/traverse");
3
+
4
+ function parseExports(code) {
5
+ const ast = parser.parse(code, {
6
+ sourceType: "module",
7
+ plugins: ["jsx", "typescript"],
8
+ });
9
+
10
+ const exports = new Set();
11
+
12
+ traverse.default(ast, {
13
+ ExportDefaultDeclaration() {
14
+ exports.add("default");
15
+ },
16
+ ExportNamedDeclaration(p) {
17
+ if (p.node.declaration) {
18
+ if (p.node.declaration.type === "FunctionDeclaration") {
19
+ exports.add(p.node.declaration.id.name);
20
+ } else if (p.node.declaration.type === "VariableDeclaration") {
21
+ p.node.declaration.declarations.forEach((d) => {
22
+ if (d.id.type === "Identifier") {
23
+ exports.add(d.id.name);
24
+ }
25
+ });
26
+ }
27
+ } else if (p.node.specifiers) {
28
+ p.node.specifiers.forEach((s) => {
29
+ if (s.type === "ExportSpecifier") {
30
+ exports.add(s.exported.name);
31
+ }
32
+ });
33
+ }
34
+ },
35
+ });
36
+
37
+ return [...exports];
38
+ }
39
+
40
+ module.exports = parseExports;
@@ -1,12 +1,15 @@
1
1
  // public/server-function-proxy.js
2
2
  import { createFromFetch } from "react-server-dom-webpack/client";
3
3
 
4
- export function createServerFunctionProxy(id) {
4
+ function createServerFunctionProxy(id) {
5
5
  return new Proxy(() => {}, {
6
6
  apply: async (_target, _thisArg, args) => {
7
7
  const res = await fetch("/____server_function____", {
8
8
  method: "POST",
9
- headers: { "Content-Type": "application/json" },
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ "x-server-function-call": "1",
12
+ },
10
13
  body: JSON.stringify({ id, args }),
11
14
  });
12
15
  if (!res.ok) throw new Error("Server function failed");
@@ -21,3 +24,10 @@ export function createServerFunctionProxy(id) {
21
24
  },
22
25
  });
23
26
  }
27
+
28
+ if (typeof window !== "undefined") {
29
+ window.__SERVER_FUNCTION_PROXY_LIB__ = { createServerFunctionProxy };
30
+ }
31
+
32
+ export { createServerFunctionProxy };
33
+ export default { createServerFunctionProxy };
@@ -6,7 +6,10 @@ export function createServerFunctionProxy(id) {
6
6
  apply: async (_target, _thisArg, args) => {
7
7
  const res = await fetch("/____server_function____", {
8
8
  method: "POST",
9
- headers: { "Content-Type": "application/json" },
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ "x-server-function-call": "1",
12
+ },
10
13
  body: JSON.stringify({ id, args }),
11
14
  });
12
15
  if (!res.ok) throw new Error("Server function failed");
@@ -41,6 +41,7 @@ const outputFolder = isDevelopment ? "public" : "dist3";
41
41
  const chokidar = require("chokidar");
42
42
  const { fileURLToPath } = require("url");
43
43
  const isWebpack = process.env.DINOU_BUILD_TOOL === "webpack";
44
+ const parseExports = require("./parse-exports.js");
44
45
  if (isDevelopment) {
45
46
  const manifestPath = path.resolve(
46
47
  process.cwd(),
@@ -217,6 +218,28 @@ if (isDevelopment) {
217
218
  }
218
219
  startManifestWatcher();
219
220
  }
221
+ let serverFunctionsManifest = null;
222
+ // const devCache = new Map(); // Para dev: Map<absolutePath, Set<exports>>
223
+
224
+ if (!isDevelopment) {
225
+ // En prod/build: cargar manifest generado
226
+ const manifestPath = path.resolve(
227
+ process.cwd(),
228
+ isWebpack
229
+ ? `${outputFolder}/server-functions-manifest.json`
230
+ : `server_functions_manifest/server-functions-manifest.json`
231
+ ); // Ajusta 'dist/' a tu outdir
232
+ if (existsSync(manifestPath)) {
233
+ serverFunctionsManifest = JSON.parse(readFileSync(manifestPath, "utf8"));
234
+ // Convertir arrays a Sets para lookups rápidos
235
+ for (const key in serverFunctionsManifest) {
236
+ serverFunctionsManifest[key] = new Set(serverFunctionsManifest[key]);
237
+ }
238
+ console.log("[server] Loaded server functions manifest");
239
+ } else {
240
+ console.error("[server] Manifest not found - falling back to file reads");
241
+ }
242
+ }
220
243
  const cookieParser = require("cookie-parser");
221
244
  const appUseCookieParser = cookieParser();
222
245
  const app = express();
@@ -235,6 +258,22 @@ app.get("/.well-known/appspecific/com.chrome.devtools.json", (req, res) => {
235
258
  });
236
259
  });
237
260
 
261
+ let cachedClientManifest = null;
262
+ if (!isDevelopment) {
263
+ // Carga inicial
264
+ cachedClientManifest = JSON.parse(
265
+ readFileSync(
266
+ path.resolve(
267
+ process.cwd(),
268
+ isWebpack
269
+ ? `${outputFolder}/react-client-manifest.json`
270
+ : `react_client_manifest/react-client-manifest.json`
271
+ ),
272
+ "utf8"
273
+ )
274
+ );
275
+ }
276
+
238
277
  app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
239
278
  try {
240
279
  const reqPath = (
@@ -242,7 +281,17 @@ app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
242
281
  ).replace("/____rsc_payload____", "");
243
282
 
244
283
  if (!isDevelopment && Object.keys({ ...req.query }).length === 0) {
245
- const payloadPath = path.join("dist2", reqPath, "rsc.rsc");
284
+ // const payloadPath = path.join("dist2", reqPath, "rsc.rsc");
285
+ const payloadPath = path.resolve(
286
+ "dist2",
287
+ reqPath.replace(/^\//, ""),
288
+ "rsc.rsc"
289
+ );
290
+ const distDir = path.resolve("dist2");
291
+
292
+ if (!payloadPath.startsWith(distDir)) {
293
+ return res.status(403).end();
294
+ }
246
295
  if (existsSync(payloadPath)) {
247
296
  res.setHeader("Content-Type", "application/octet-stream");
248
297
  const readStream = createReadStream(payloadPath);
@@ -259,16 +308,19 @@ app.get(/^\/____rsc_payload____\/.*\/?$/, async (req, res) => {
259
308
  { ...req.cookies },
260
309
  isDevelopment
261
310
  );
262
- const manifest = JSON.parse(
263
- readFileSync(
264
- path.resolve(
265
- isWebpack
266
- ? `${outputFolder}/react-client-manifest.json`
267
- : `react_client_manifest/react-client-manifest.json`
268
- ),
269
- "utf8"
270
- )
271
- );
311
+ const manifest = isDevelopment
312
+ ? JSON.parse(
313
+ readFileSync(
314
+ path.resolve(
315
+ process.cwd(),
316
+ isWebpack
317
+ ? `${outputFolder}/react-client-manifest.json`
318
+ : `react_client_manifest/react-client-manifest.json`
319
+ ),
320
+ "utf8"
321
+ )
322
+ )
323
+ : cachedClientManifest;
272
324
 
273
325
  const { pipe } = renderToPipeableStream(jsx, manifest);
274
326
  pipe(res);
@@ -284,17 +336,20 @@ app.post(/^\/____rsc_payload_error____\/.*\/?$/, async (req, res) => {
284
336
  req.path.endsWith("/") ? req.path : req.path + "/"
285
337
  ).replace("/____rsc_payload_error____", "");
286
338
  const jsx = await getErrorJSX(reqPath, { ...req.query }, req.body.error);
287
- const manifest = readFileSync(
288
- path.resolve(
289
- process.cwd(),
290
- isWebpack
291
- ? `${outputFolder}/react-client-manifest.json`
292
- : `react_client_manifest/react-client-manifest.json`
293
- ),
294
- "utf8"
295
- );
296
- const moduleMap = JSON.parse(manifest);
297
- const { pipe } = renderToPipeableStream(jsx, moduleMap);
339
+ const manifest = isDevelopment
340
+ ? JSON.parse(
341
+ readFileSync(
342
+ path.resolve(
343
+ process.cwd(),
344
+ isWebpack
345
+ ? `${outputFolder}/react-client-manifest.json`
346
+ : `react_client_manifest/react-client-manifest.json`
347
+ ),
348
+ "utf8"
349
+ )
350
+ )
351
+ : cachedClientManifest;
352
+ const { pipe } = renderToPipeableStream(jsx, manifest);
298
353
  pipe(res);
299
354
  } catch (error) {
300
355
  console.error("Error rendering RSC:", error);
@@ -337,47 +392,143 @@ app.get(/^\/.*\/?$/, (req, res) => {
337
392
 
338
393
  app.post("/____server_function____", async (req, res) => {
339
394
  try {
395
+ // 1. Verificar Origin (Prevenir llamadas desde otros dominios)
396
+ const origin = req.headers.origin;
397
+ const host = req.headers.host;
398
+
399
+ // Nota: En local a veces origin es undefined o null, permitirlo en dev si es necesario
400
+ if (!isDevelopment && origin && !origin.includes(host)) {
401
+ return res.status(403).json({ error: "Invalid Origin" });
402
+ }
403
+
404
+ // 2. Verificar Header Personalizado (Defensa CSRF robusta)
405
+ // Asegúrate de que tu cliente (server-function-proxy.js) envíe este header
406
+ if (!req.headers["x-server-function-call"]) {
407
+ return res.status(403).json({ error: "Missing security header" });
408
+ }
340
409
  const { id, args } = req.body;
410
+
411
+ // Validación básica de inputs: id debe ser string, args un array
412
+ if (typeof id !== "string" || !Array.isArray(args)) {
413
+ return res.status(400).json({ error: "Invalid request body" });
414
+ }
415
+
341
416
  const [fileUrl, exportName] = id.split("#");
342
417
 
343
- let relativePath = fileUrl.replace(/^file:\/\/\/?/, "");
418
+ // Validar fileUrl: debe empezar con 'file://' y no contener caracteres sospechosos
419
+ if (!fileUrl.startsWith("file://")) {
420
+ return res.status(400).json({ error: "Invalid file URL format" });
421
+ }
422
+
423
+ // Extraer relativePath y normalizarlo (elimina 'file://' y posibles '/')
424
+ let relativePath = fileUrl.replace(/^file:\/\/\/?/, "").trim();
425
+ if (relativePath.startsWith("/") || relativePath.includes("..")) {
426
+ return res
427
+ .status(400)
428
+ .json({ error: "Invalid path: no absolute or traversal allowed" });
429
+ }
430
+ // console.log("relPath", relativePath);
431
+ // Restringir a carpeta 'src/': prepend 'src/' si no está, y resolver absolutePath
432
+ if (!relativePath.startsWith("src/") && !relativePath.startsWith("src\\")) {
433
+ relativePath = path.join("src", relativePath);
434
+ }
344
435
  const absolutePath = path.resolve(process.cwd(), relativePath);
345
436
 
437
+ // Verificar que absolutePath esté estrictamente dentro de 'src/'
438
+ const srcDir = path.resolve(process.cwd(), "src");
439
+ if (!absolutePath.startsWith(srcDir + path.sep)) {
440
+ return res
441
+ .status(403)
442
+ .json({ error: "Access denied: file outside src directory" });
443
+ }
444
+ // console.log("absPath", absolutePath);
445
+ // Verificar que el archivo exista
446
+ if (!existsSync(absolutePath)) {
447
+ return res.status(404).json({ error: "File not found" });
448
+ }
449
+
450
+ let allowedExports;
451
+ if (serverFunctionsManifest) {
452
+ // Prod: usar manifest (relativePath ya está normalizado)
453
+ allowedExports = serverFunctionsManifest[relativePath];
454
+ } else {
455
+ // Dev: usar cache o verificar archivo
456
+ // allowedExports = devCache.get(absolutePath);
457
+ // if (!allowedExports) {
458
+ const fileContent = readFileSync(absolutePath, "utf8"); // Solo lee una vez
459
+ const firstLine = fileContent.trim().split("\n")[0].trim();
460
+ if (
461
+ !firstLine.startsWith('"use server"') &&
462
+ !firstLine.startsWith("'use server'")
463
+ ) {
464
+ return res
465
+ .status(403)
466
+ .json({ error: "Not a valid server function file" });
467
+ }
468
+ // Parsear exports (necesitas implementar parseExports en server si no lo tienes)
469
+ const exports = parseExports(fileContent); // Asume que mueves parseExports a un util compartido
470
+ allowedExports = new Set(exports);
471
+ // devCache.set(absolutePath, allowedExports);
472
+ // }
473
+ }
474
+
475
+ // Validar exportName contra allowedExports
476
+ if (
477
+ !exportName ||
478
+ (exportName !== "default" && !allowedExports.has(exportName))
479
+ ) {
480
+ return res.status(400).json({ error: "Invalid export name" });
481
+ }
482
+
483
+ // Proceder con la importación (usando tu importModule)
346
484
  const mod = await importModule(absolutePath);
347
485
 
486
+ // Validar exportName: solo permitir 'default' u otros si defines una whitelist
487
+ if (!exportName || (exportName !== "default" && !mod[exportName])) {
488
+ return res.status(400).json({ error: "Invalid export name" });
489
+ }
348
490
  const fn = exportName === "default" ? mod.default : mod[exportName];
349
491
 
350
492
  if (typeof fn !== "function") {
351
493
  return res.status(400).json({ error: "Export is not a function" });
352
494
  }
353
495
 
496
+ // Ejecutar la función con context
354
497
  const context = { req, res };
355
498
  args.push(context);
499
+ if (args.length > fn.length + 1) {
500
+ return res.status(400).json({ error: "Invalid args length" });
501
+ }
356
502
  const result = await fn(...args);
357
503
 
504
+ // Manejo del resultado (igual que antes, pero con chequeos extras si es necesario)
358
505
  if (
359
506
  result &&
360
507
  result.$$typeof === Symbol.for("react.transitional.element")
361
508
  ) {
362
509
  res.setHeader("Content-Type", "text/x-component");
363
- const manifest = readFileSync(
364
- path.resolve(
365
- process.cwd(),
366
- isWebpack
367
- ? `${outputFolder}/react-client-manifest.json`
368
- : `react_client_manifest/react-client-manifest.json`
369
- ),
370
- "utf8"
510
+ const manifestPath = path.resolve(
511
+ process.cwd(),
512
+ isWebpack
513
+ ? `${outputFolder}/react-client-manifest.json`
514
+ : `react_client_manifest/react-client-manifest.json`
371
515
  );
372
- const moduleMap = JSON.parse(manifest);
373
- const { pipe } = renderToPipeableStream(result, moduleMap);
516
+ // Verificar que el manifest exista para evitar errores
517
+ if (!existsSync(manifestPath)) {
518
+ return res.status(500).json({ error: "Manifest not found" });
519
+ }
520
+ const manifest = isDevelopment
521
+ ? JSON.parse(readFileSync(manifestPath, "utf8"))
522
+ : cachedClientManifest;
523
+ const { pipe } = renderToPipeableStream(result, manifest);
374
524
  pipe(res);
375
525
  } else {
376
526
  res.json(result);
377
527
  }
378
528
  } catch (err) {
379
- console.error(`Server function error [${req.body.id}]:`, err);
380
- res.status(500).json({ error: err.message });
529
+ console.error(`Server function error [${req.body?.id}]:`, err);
530
+ // En producción, no envíes err.message completo para evitar leaks
531
+ res.status(500).json({ error: "Internal server error" });
381
532
  }
382
533
  });
383
534
 
@@ -11,6 +11,7 @@ const __dirname = path.dirname(__filename);
11
11
  const outdir = "dist3";
12
12
  await fs.rm(outdir, { recursive: true, force: true });
13
13
  await fs.rm("react_client_manifest", { recursive: true, force: true });
14
+ await fs.rm("server_functions_manifest", { recursive: true, force: true });
14
15
 
15
16
  const frameworkEntryPoints = {
16
17
  main: path.resolve(__dirname, "../core/client.jsx"),
@@ -14,6 +14,7 @@ const __dirname = path.dirname(__filename);
14
14
  const outdir = "public";
15
15
  await fs.rm(outdir, { recursive: true, force: true });
16
16
  await fs.rm("react_client_manifest", { recursive: true, force: true });
17
+ await fs.rm("server_functions_manifest", { recursive: true, force: true });
17
18
 
18
19
  let currentCtx = null; // Track the active esbuild context
19
20
  let debounceTimer = null; // For debouncing recreations
@@ -1,62 +1,29 @@
1
1
  import path from "path";
2
- import parser from "@babel/parser";
3
- import traverse from "@babel/traverse";
4
- import fs from "fs";
5
-
6
- function parseExports(code) {
7
- const ast = parser.parse(code, {
8
- sourceType: "module",
9
- plugins: ["jsx", "typescript"],
10
- });
11
-
12
- const exports = new Set();
13
-
14
- traverse.default(ast, {
15
- ExportDefaultDeclaration() {
16
- exports.add("default");
17
- },
18
- ExportNamedDeclaration(p) {
19
- if (p.node.declaration) {
20
- if (p.node.declaration.type === "FunctionDeclaration") {
21
- exports.add(p.node.declaration.id.name);
22
- } else if (p.node.declaration.type === "VariableDeclaration") {
23
- p.node.declaration.declarations.forEach((d) => {
24
- if (d.id.type === "Identifier") {
25
- exports.add(d.id.name);
26
- }
27
- });
28
- }
29
- } else if (p.node.specifiers) {
30
- p.node.specifiers.forEach((s) => {
31
- if (s.type === "ExportSpecifier") {
32
- exports.add(s.exported.name);
33
- }
34
- });
35
- }
36
- },
37
- });
38
-
39
- return [...exports];
40
- }
2
+ import fs from "node:fs/promises";
3
+ import parseExports from "../../core/parse-exports.js";
41
4
 
42
5
  export default function serverFunctionsPlugin(manifestData = {}) {
43
6
  return {
44
7
  name: "server-functions-proxy",
45
8
  setup(build) {
46
9
  const root = process.cwd();
10
+ const serverFunctions = new Map(); // Recolectar aquí: Map<relativePath, Set<exports>>
47
11
 
48
12
  // 1. TRANSFORM FILES DURING BUILD
49
13
  build.onLoad({ filter: /\.[jt]sx?$/ }, async (args) => {
50
- const code = await fs.promises.readFile(args.path, "utf8");
14
+ const code = await fs.readFile(args.path, "utf8");
51
15
 
52
16
  if (!code.trim().startsWith('"use server"')) return null;
53
17
 
54
18
  const exports = parseExports(code);
55
19
  if (exports.length === 0) return null;
56
20
 
57
- const fileUrl = `file:///${path.relative(root, args.path)}`;
21
+ const relativePath = path.relative(root, args.path);
22
+ serverFunctions.set(relativePath, new Set(exports)); // Guardar exports como Set para uniqueness
23
+
24
+ const fileUrl = `file:///${relativePath}`;
58
25
 
59
- // Proxy code
26
+ // Proxy code (igual que antes)
60
27
  let proxyCode = `
61
28
  import { createServerFunctionProxy } from "/__SERVER_FUNCTION_PROXY__";
62
29
  `;
@@ -82,9 +49,8 @@ export default function serverFunctionsPlugin(manifestData = {}) {
82
49
  };
83
50
  });
84
51
 
85
- // 2. REPLACE PLACEHOLDER AFTER BUILD
52
+ // 2. REPLACE PLACEHOLDER AND GENERATE MANIFEST AFTER BUILD
86
53
  build.onEnd(async (result) => {
87
- // console.log("[server-functions-proxy] manifest:", manifestData);
88
54
  const hashedProxy =
89
55
  "/" +
90
56
  (manifestData["serverFunctionProxy.js"] || "serverFunctionProxy.js");
@@ -98,12 +64,26 @@ export default function serverFunctionsPlugin(manifestData = {}) {
98
64
  /\/__SERVER_FUNCTION_PROXY__/g,
99
65
  hashedProxy
100
66
  );
101
- // console.log(
102
- // `[server-functions-proxy] Replaced __SERVER_FUNCTION_PROXY__ in ${outputFile.path}`
103
- // );
104
67
  outputFile.contents = new TextEncoder().encode(newCode);
105
68
  }
106
69
  }
70
+
71
+ // Generar manifest: convertir Map a objeto simple
72
+ const manifestObj = {};
73
+ for (const [path, exportsSet] of serverFunctions.entries()) {
74
+ manifestObj[path] = Array.from(exportsSet);
75
+ }
76
+
77
+ // Escribir el manifest en el output dir (ej. mismo lugar que otros assets)
78
+ const manifestPath = path.join(
79
+ "server_functions_manifest",
80
+ "server-functions-manifest.json"
81
+ );
82
+ await fs.mkdir(path.dirname(manifestPath), { recursive: true });
83
+ await fs.writeFile(manifestPath, JSON.stringify(manifestObj, null, 2));
84
+ // console.log(
85
+ // `[server-functions-proxy] Generated manifest at ${manifestPath}`
86
+ // );
107
87
  });
108
88
  },
109
89
  };
@@ -1,45 +1,13 @@
1
1
  // rollup-plugin-server-functions.js
2
2
  const path = require("path");
3
- const parser = require("@babel/parser");
4
- const traverse = require("@babel/traverse").default;
3
+ const fs = require("fs/promises");
5
4
  const manifestGeneratorPlugin = require("./manifest-generator-plugin");
6
-
7
- function parseExports(code) {
8
- const ast = parser.parse(code, {
9
- sourceType: "module",
10
- plugins: ["jsx", "typescript"],
11
- });
12
- const exports = new Set();
13
-
14
- traverse(ast, {
15
- ExportDefaultDeclaration() {
16
- exports.add("default");
17
- },
18
- ExportNamedDeclaration(p) {
19
- if (p.node.declaration) {
20
- if (p.node.declaration.type === "FunctionDeclaration") {
21
- exports.add(p.node.declaration.id.name);
22
- } else if (p.node.declaration.type === "VariableDeclaration") {
23
- p.node.declaration.declarations.forEach((d) => {
24
- if (d.id.type === "Identifier") {
25
- exports.add(d.id.name);
26
- }
27
- });
28
- }
29
- } else if (p.node.specifiers) {
30
- p.node.specifiers.forEach((s) => {
31
- if (s.type === "ExportSpecifier") {
32
- exports.add(s.exported.name);
33
- }
34
- });
35
- }
36
- },
37
- });
38
-
39
- return Array.from(exports);
40
- }
5
+ const parseExports = require("../../core/parse-exports.js");
41
6
 
42
7
  function serverFunctionsPlugin() {
8
+ const root = process.cwd();
9
+ const serverFunctions = new Map(); // Recolectar aquí: Map<relativePath, Set<exports>>
10
+
43
11
  return {
44
12
  name: "server-functions-proxy",
45
13
  transform(code, id) {
@@ -48,7 +16,10 @@ function serverFunctionsPlugin() {
48
16
  const exports = parseExports(code);
49
17
  if (exports.length === 0) return null;
50
18
 
51
- const fileUrl = `file:///${path.relative(process.cwd(), id)}`;
19
+ const relativePath = path.relative(root, id);
20
+ serverFunctions.set(relativePath, new Set(exports)); // Guardar exports como Set para uniqueness
21
+
22
+ const fileUrl = `file:///${relativePath}`;
52
23
 
53
24
  // Generamos un módulo que exporta proxies en lugar del código real
54
25
  let proxyCode = `
@@ -90,6 +61,26 @@ function serverFunctionsPlugin() {
90
61
  );
91
62
  }
92
63
  }
64
+
65
+ // Generar manifest: convertir Map a objeto simple
66
+ const manifestObj = {};
67
+ for (const [relPath, exportsSet] of serverFunctions.entries()) {
68
+ manifestObj[relPath] = Array.from(exportsSet);
69
+ }
70
+
71
+ // Escribir el manifest en la carpeta especificada (ej. mismo lugar que otros assets)
72
+ const manifestPath = path.join(
73
+ "server_functions_manifest",
74
+ "server-functions-manifest.json"
75
+ );
76
+ fs.mkdir(path.dirname(manifestPath), { recursive: true })
77
+ .then(() =>
78
+ fs.writeFile(manifestPath, JSON.stringify(manifestObj, null, 2))
79
+ )
80
+ .then(() => {
81
+ // console.log(`[rollup-server-functions] Generated manifest at ${manifestPath}`);
82
+ })
83
+ .catch(console.error);
93
84
  },
94
85
  };
95
86
  }
@@ -60,7 +60,11 @@ module.exports = async function () {
60
60
  ],
61
61
  plugins: [
62
62
  del({
63
- targets: [`${outputDirectory}/*`, "react_client_manifest/*"],
63
+ targets: [
64
+ `${outputDirectory}/*`,
65
+ "react_client_manifest/*",
66
+ "server_functions_manifest/*",
67
+ ],
64
68
  runOnce: true,
65
69
  hook: "buildStart",
66
70
  }),
@@ -1,69 +1,78 @@
1
- // server-functions-loader.js
2
- const parser = require("@babel/parser");
3
- const traverse = require("@babel/traverse").default;
1
+ // server-functions-loader-simple.js
4
2
  const path = require("path");
3
+ const parseExports = require("../../core/parse-exports.js");
5
4
 
6
- function parseExports(code) {
7
- const ast = parser.parse(code, {
8
- sourceType: "module",
9
- plugins: ["jsx", "typescript"],
10
- });
5
+ module.exports = function (source) {
6
+ // Detect "use server"
7
+ const lines = source.split("\n");
8
+ let hasUseServer = false;
11
9
 
12
- const exports = new Set();
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ if (
13
+ trimmed.startsWith('"use server"') ||
14
+ trimmed.startsWith("'use server'")
15
+ ) {
16
+ hasUseServer = true;
17
+ break;
18
+ }
19
+ }
13
20
 
14
- traverse(ast, {
15
- ExportDefaultDeclaration() {
16
- exports.add("default");
17
- },
18
- ExportNamedDeclaration(p) {
19
- if (p.node.declaration) {
20
- if (p.node.declaration.type === "FunctionDeclaration") {
21
- exports.add(p.node.declaration.id.name);
22
- } else if (p.node.declaration.type === "VariableDeclaration") {
23
- p.node.declaration.declarations.forEach((d) => {
24
- if (d.id.type === "Identifier") {
25
- exports.add(d.id.name);
26
- }
27
- });
28
- }
29
- } else if (p.node.specifiers) {
30
- p.node.specifiers.forEach((s) => {
31
- if (s.type === "ExportSpecifier") {
32
- exports.add(s.exported.name);
33
- }
34
- });
35
- }
36
- },
37
- });
21
+ if (!hasUseServer) return source;
38
22
 
39
- return Array.from(exports);
40
- }
23
+ const exports = parseExports(source);
24
+ if (exports.length === 0) return source;
41
25
 
42
- module.exports = function serverFunctionsLoader(source) {
43
- if (!source.trim().startsWith('"use server"')) return source;
26
+ // Build IDs
27
+ const moduleId = this.resourcePath;
28
+ const relativePath = path.relative(process.cwd(), moduleId);
29
+ const normalizedPath = relativePath.replace(/\\/g, "/");
44
30
 
45
- const callback = this.async();
46
- const exports = parseExports(source);
47
- if (exports.length === 0) return callback(null, source);
31
+ const fileUrl = `file:///${normalizedPath}`;
48
32
 
49
- const fileUrl = `file:///${path.relative(process.cwd(), this.resourcePath)}`;
33
+ //
34
+ // IMPORTANT: dynamic import instead of static import
35
+ //
36
+ // Webpack will NOT try to resolve "__SERVER_FUNCTION_PROXY__"
37
+ // as a module → it will remain a string → replaced later → browser loads it.
50
38
 
51
- let out = `import { createServerFunctionProxy } from "__SERVER_FUNCTION_PROXY__";\n`;
39
+ let proxyCode = `
40
+ const loadProxy = new Function('return import("/"+"__SERVER_FUNCTION_PROXY__")');
41
+ `;
52
42
 
53
- for (const name of exports) {
54
- const key =
55
- name === "default" ? `${fileUrl}#default` : `${fileUrl}#${name}`;
43
+ for (const exp of exports) {
44
+ const key = exp === "default" ? `${fileUrl}#default` : `${fileUrl}#${exp}`;
56
45
 
57
- if (name === "default") {
58
- out += `export default createServerFunctionProxy(${JSON.stringify(
59
- key
60
- )});\n`;
46
+ if (exp === "default") {
47
+ proxyCode += `
48
+ export default (...args) =>
49
+ loadProxy().then(mod =>
50
+ (mod.default ?? mod ?? window.__SERVER_FUNCTION_PROXY_LIB__).createServerFunctionProxy(${JSON.stringify(
51
+ key
52
+ )})(...args)
53
+ );
54
+ `;
61
55
  } else {
62
- out += `export const ${name} = createServerFunctionProxy(${JSON.stringify(
63
- key
64
- )});\n`;
56
+ proxyCode += `
57
+ export const ${exp} = (...args) =>
58
+ loadProxy().then(mod => (mod.default ?? mod ?? window.__SERVER_FUNCTION_PROXY_LIB__).createServerFunctionProxy(${JSON.stringify(
59
+ key
60
+ )})(...args)
61
+ );
62
+ `;
65
63
  }
66
64
  }
67
65
 
68
- callback(null, out);
66
+ // Emit manifest entry
67
+ const manifestEntry = {
68
+ path: normalizedPath,
69
+ exports: exports,
70
+ };
71
+
72
+ this.emitFile(
73
+ `server-functions/${normalizedPath}.json`,
74
+ JSON.stringify(manifestEntry, null, 2)
75
+ );
76
+
77
+ return proxyCode;
69
78
  };
@@ -1,44 +1,82 @@
1
- // ServerFunctionsPlugin.js
2
- class ServerFunctionsPlugin {
3
- constructor({ manifest }) {
4
- this.manifest = manifest;
1
+ // webpack-server-functions-plugin-simple.js
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const manifestGeneratorPlugin = require("./manifest-generator-plugin");
5
+
6
+ class WebpackServerFunctionsPluginSimple {
7
+ constructor() {
8
+ this.serverFunctions = new Map();
5
9
  }
6
10
 
7
11
  apply(compiler) {
8
- const pluginName = "ServerFunctionsPlugin";
9
-
10
- compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
11
- compilation.hooks.processAssets.tap(
12
- {
13
- name: pluginName,
14
- stage: compilation.constructor.PROCESS_ASSETS_STAGE_OPTIMIZE,
15
- },
16
- (assets) => {
17
- const proxyPath =
18
- "/" +
19
- (this.manifest["serverFunctionProxy.js"] ||
20
- "serverFunctionProxy.js");
21
-
22
- for (const filename of Object.keys(assets)) {
23
- let source = assets[filename].source();
24
- if (typeof source !== "string") continue;
25
-
26
- if (!source.includes("__SERVER_FUNCTION_PROXY__")) continue;
27
-
28
- const replaced = source.replace(
29
- /__SERVER_FUNCTION_PROXY__/g,
30
- proxyPath
31
- );
12
+ const { webpack } = compiler;
13
+ const { Compilation, sources } = webpack;
14
+
15
+ // Recopilar archivos generados por el loader
16
+ compiler.hooks.thisCompilation.tap(
17
+ "WebpackServerFunctionsPluginSimple",
18
+ (compilation) => {
19
+ compilation.hooks.processAssets.tap(
20
+ {
21
+ name: "WebpackServerFunctionsPluginSimple",
22
+ stage: Compilation.PROCESS_ASSETS_STAGE_REPORT,
23
+ },
24
+ (assets) => {
25
+ // 1. Reemplazar placeholder
26
+ const manifest = manifestGeneratorPlugin.manifestData;
27
+ const hashedPath =
28
+ // "/" +
29
+ manifest["serverFunctionProxy.js"] || "serverFunctionProxy.js";
30
+
31
+ for (const [filename, asset] of Object.entries(assets)) {
32
+ if (filename.endsWith(".js")) {
33
+ let code = asset.source();
34
+ if (code.includes("__SERVER_FUNCTION_PROXY__")) {
35
+ code = code.replace(/__SERVER_FUNCTION_PROXY__/g, hashedPath);
36
+ compilation.updateAsset(
37
+ filename,
38
+ new sources.RawSource(code)
39
+ );
40
+ }
41
+ }
42
+ }
43
+
44
+ // 2. Recopilar todos los archivos de server functions
45
+ const serverFunctionsManifest = {};
32
46
 
33
- compilation.updateAsset(filename, (old) => ({
34
- source: () => replaced,
35
- size: () => replaced.length,
36
- }));
47
+ for (const [filename, asset] of Object.entries(assets)) {
48
+ if (
49
+ filename.startsWith("server-functions/") &&
50
+ filename.endsWith(".json")
51
+ ) {
52
+ try {
53
+ const content = asset.source();
54
+ const entry = JSON.parse(content);
55
+ serverFunctionsManifest[entry.path] = entry.exports;
56
+
57
+ // Eliminar este archivo temporal
58
+ delete assets[filename];
59
+ } catch (e) {
60
+ // Ignorar errores de parsing
61
+ }
62
+ }
63
+ }
64
+
65
+ // 3. Generar manifest final
66
+ const manifestContent = JSON.stringify(
67
+ serverFunctionsManifest,
68
+ null,
69
+ 2
70
+ );
71
+ compilation.emitAsset(
72
+ "server-functions-manifest.json",
73
+ new sources.RawSource(manifestContent)
74
+ );
37
75
  }
38
- }
39
- );
40
- });
76
+ );
77
+ }
78
+ );
41
79
  }
42
80
  }
43
81
 
44
- module.exports = ServerFunctionsPlugin;
82
+ module.exports = WebpackServerFunctionsPluginSimple;
@@ -51,11 +51,24 @@ module.exports = async () => {
51
51
  {}
52
52
  ),
53
53
  },
54
+ experiments: {
55
+ outputModule: true,
56
+ },
54
57
  output: {
55
58
  path: path.resolve(process.cwd(), outputDirectory),
56
59
  filename: "[name]-[contenthash].js",
57
60
  publicPath: "/",
58
61
  clean: true,
62
+ library: {
63
+ type: "module",
64
+ },
65
+ environment: {
66
+ module: true,
67
+ },
68
+ // module: true,
69
+ chunkFormat: "module", // Ensures non-entry chunks (like serverFunctionProxy) output as ESM
70
+ // // Optional: If webpack renames to .mjs, force .js
71
+ // chunkFilename: "[name]-[contenthash].js",
59
72
  },
60
73
  module: {
61
74
  noParse: [/dist3/, /public/],
@@ -65,18 +78,7 @@ module.exports = async () => {
65
78
  // include: path.resolve(process.cwd(), "dist3"),
66
79
  // use: "null-loader",
67
80
  // },
68
- {
69
- test: /\.[jt]sx?$/,
70
- include: path.resolve(process.cwd(), "src"),
71
- use: [
72
- {
73
- loader: path.resolve(
74
- __dirname,
75
- "./loaders/server-functions-loader.js"
76
- ),
77
- },
78
- ],
79
- },
81
+
80
82
  {
81
83
  test: /\.(js|jsx|ts|tsx)$/,
82
84
  include: [
@@ -91,7 +93,6 @@ module.exports = async () => {
91
93
  "@babel/preset-typescript",
92
94
  ],
93
95
  plugins: [
94
- "@babel/plugin-transform-modules-commonjs",
95
96
  "@babel/plugin-syntax-import-meta",
96
97
  // isDevelopment && require.resolve("react-refresh/babel"),
97
98
  ].filter(Boolean),
@@ -103,6 +104,18 @@ module.exports = async () => {
103
104
  path.resolve(process.cwd(), "public"),
104
105
  ],
105
106
  },
107
+ {
108
+ test: /\.[jt]sx?$/,
109
+ include: path.resolve(process.cwd(), "src"),
110
+ use: [
111
+ {
112
+ loader: path.resolve(
113
+ __dirname,
114
+ "./loaders/server-functions-loader.js"
115
+ ),
116
+ },
117
+ ],
118
+ },
106
119
  {
107
120
  test: /\.module\.css$/,
108
121
  use: [
@@ -198,9 +211,11 @@ module.exports = async () => {
198
211
  ]
199
212
  : [],
200
213
  },
201
- externals: {
202
- __SERVER_FUNCTION_PROXY__: "__SERVER_FUNCTION_PROXY__",
203
- },
214
+ // externals: {
215
+ // // __SERVER_FUNCTION_PROXY__: "__SERVER_FUNCTION_PROXY__",
216
+ // // "/__SERVER_FUNCTION_PROXY__": "/__SERVER_FUNCTION_PROXY__",
217
+ // // serverFunctionProxy: "/serverFunctionProxy.js",
218
+ // },
204
219
  optimization: {
205
220
  splitChunks: {
206
221
  cacheGroups: {
@@ -216,6 +231,10 @@ module.exports = async () => {
216
231
  watchOptions: {
217
232
  ignored: ["public/", "dist3/"],
218
233
  },
234
+ stats: "normal", // o 'verbose' en dev
235
+ infrastructureLogging: {
236
+ level: "info",
237
+ },
219
238
  ...(isDevelopment
220
239
  ? {
221
240
  devServer: {
@@ -232,6 +251,7 @@ module.exports = async () => {
232
251
  changeOrigin: true,
233
252
  },
234
253
  ],
254
+ client: false,
235
255
  },
236
256
  }
237
257
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dinou",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "Dinou is a modern React 19 framework with React Server Components, Server Functions, and streaming SSR.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "@babel/traverse": "^7.28.3",
42
42
  "@esbuild-plugins/tsconfig-paths": "^0.1.2",
43
43
  "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
44
- "@roggc/react-server-dom-esm": "^0.0.0-380778d2-20251207",
44
+ "@roggc/react-server-dom-esm": "^0.0.0-b061b597-20251212",
45
45
  "@rollup/plugin-babel": "^6.0.4",
46
46
  "@rollup/plugin-commonjs": "^28.0.6",
47
47
  "@rollup/plugin-json": "^6.1.0",
@@ -70,7 +70,7 @@
70
70
  "postcss-loader": "^8.2.0",
71
71
  "postcss-modules": "^6.0.1",
72
72
  "react-refresh": "^0.17.0",
73
- "react-server-dom-webpack": "^19.2.1",
73
+ "react-server-dom-webpack": "^19.2.3",
74
74
  "rollup": "^4.46.2",
75
75
  "rollup-plugin-copy": "^3.5.0",
76
76
  "rollup-plugin-delete": "^3.0.1",
@@ -85,7 +85,7 @@
85
85
  "ws": "^8.18.3"
86
86
  },
87
87
  "peerDependencies": {
88
- "react": ">=19.2.1",
89
- "react-dom": ">=19.2.1"
88
+ "react": ">=19.2.3",
89
+ "react-dom": ">=19.2.3"
90
90
  }
91
91
  }