dinou 6.1.0 → 7.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +9461 -1080
  3. package/cli.js +392 -169
  4. package/dinou/adapters/bun.js +62 -0
  5. package/dinou/adapters/cloudflare.js +65 -0
  6. package/dinou/adapters/deno.js +48 -0
  7. package/dinou/adapters/netlify.js +85 -0
  8. package/dinou/adapters/node.mjs +31 -0
  9. package/dinou/bun/build.mjs +1077 -0
  10. package/dinou/cloudflare/build.mjs +1121 -0
  11. package/dinou/cloudflare/shims/child_process.js +14 -0
  12. package/dinou/cloudflare/shims/empty.js +2 -0
  13. package/dinou/cloudflare/shims/fs.js +99 -0
  14. package/dinou/cloudflare/shims/os.js +29 -0
  15. package/dinou/config.js +1 -0
  16. package/dinou/config.mjs +2 -0
  17. package/dinou/constants.js +29 -4
  18. package/dinou/core/babel-esm-loader.js +66 -37
  19. package/dinou/core/bailout-proxy.js +61 -0
  20. package/dinou/core/build-static-pages.js +309 -157
  21. package/dinou/core/cache-revalidate.js +471 -144
  22. package/dinou/core/cache-slot.js +387 -0
  23. package/dinou/core/client-redirect.jsx +12 -7
  24. package/dinou/core/client-webpack.jsx +457 -82
  25. package/dinou/core/client.jsx +452 -82
  26. package/dinou/core/config.js +11 -0
  27. package/dinou/core/dinou-paths.js +52 -0
  28. package/dinou/core/edge-ssr.js +122 -0
  29. package/dinou/core/export-static.mjs +64 -0
  30. package/dinou/core/generate-route-types.js +252 -0
  31. package/dinou/core/generate-static-page.js +55 -94
  32. package/dinou/core/generate-static-rsc.js +47 -15
  33. package/dinou/core/generate-static.js +212 -10
  34. package/dinou/core/generating-isg.js +5 -0
  35. package/dinou/core/get-asset-from-manifest.js +9 -3
  36. package/dinou/core/get-error-jsx.js +221 -41
  37. package/dinou/core/get-file-path-and-dynamic-params.js +9 -13
  38. package/dinou/core/get-jsx.js +172 -95
  39. package/dinou/core/handler.js +2296 -0
  40. package/dinou/core/http-adapter.js +115 -0
  41. package/dinou/core/import-module.js +83 -53
  42. package/dinou/core/layout-functions.js +181 -0
  43. package/dinou/core/link.jsx +18 -6
  44. package/dinou/core/load-dinou-config.js +60 -0
  45. package/dinou/core/manifest-provider.js +261 -0
  46. package/dinou/core/navigation-utils.js +4 -0
  47. package/dinou/core/navigation.js +50 -39
  48. package/dinou/core/parse-exports.js +207 -2
  49. package/dinou/core/ppr-context.js +105 -0
  50. package/dinou/core/ppr-runtime.js +263 -0
  51. package/dinou/core/redirect.jsx +20 -19
  52. package/dinou/core/render-jsx-to-client-jsx.js +133 -15
  53. package/dinou/core/request-context.js +47 -13
  54. package/dinou/core/resolve-module-ppr.js +32 -0
  55. package/dinou/core/revalidating.js +28 -9
  56. package/dinou/core/route-generator.js +107 -0
  57. package/dinou/core/rsc-renderer.js +90 -0
  58. package/dinou/core/run-ssg.js +19 -0
  59. package/dinou/core/scan-dependency-components.js +218 -0
  60. package/dinou/core/server-function-proxy-webpack.js +3 -0
  61. package/dinou/core/server-function-proxy.js +3 -0
  62. package/dinou/core/slot.js +302 -0
  63. package/dinou/core/status-manifest.js +41 -1
  64. package/dinou/core/storage-adapter.js +522 -0
  65. package/dinou/core/url-resolver.js +16 -16
  66. package/dinou/core/vfs.js +139 -68
  67. package/dinou/deno/build.mjs +1112 -0
  68. package/dinou/deno/seed-kv.js +115 -0
  69. package/dinou/esbuild/build.mjs +51 -1
  70. package/dinou/esbuild/dev.mjs +290 -224
  71. package/dinou/esbuild/helpers-esbuild/get-config-esbuild-prod.mjs +3 -2
  72. package/dinou/esbuild/helpers-esbuild/get-config-esbuild.mjs +104 -16
  73. package/dinou/esbuild/helpers-esbuild/get-esbuild-entries.mjs +31 -19
  74. package/dinou/esbuild/helpers-esbuild/strip-namespace.mjs +9 -0
  75. package/dinou/esbuild/helpers-esbuild/swc-disk-cache.mjs +225 -0
  76. package/dinou/esbuild/helpers-esbuild/update-manifest-for-module.mjs +7 -0
  77. package/dinou/esbuild/helpers-esbuild/write.mjs +111 -2
  78. package/dinou/esbuild/plugins-esbuild/assets-plugin.mjs +236 -161
  79. package/dinou/esbuild/plugins-esbuild/css-processor-plugin.mjs +101 -5
  80. package/dinou/esbuild/plugins-esbuild/react-client-manifest-plugin.mjs +63 -24
  81. package/dinou/esbuild/plugins-esbuild/server-functions-plugin.mjs +177 -56
  82. package/dinou/esbuild/plugins-esbuild/skip-missing-entry-points-plugin.mjs +3 -0
  83. package/dinou/esbuild/plugins-esbuild/stable-chunk-names-and-maps-plugin.mjs +182 -137
  84. package/dinou/esbuild/plugins-postcss/postcss-extract-plugin.js +38 -11
  85. package/dinou/esbuild/react-refresh/esm-hmr/client.mjs +55 -8
  86. package/dinou/esbuild/react-refresh/esm-hmr/server.js +1 -1
  87. package/dinou/esbuild/react-refresh/esm-hmr-plugin.mjs +238 -101
  88. package/dinou/esbuild/react-refresh/is-react-refresh-boundary.mjs +18 -4
  89. package/dinou/index.d.ts +315 -11
  90. package/dinou/index.js +12 -6
  91. package/dinou/index.mjs +9 -0
  92. package/dinou/node/build.mjs +195 -0
  93. package/dinou/node/bundle-dual-engine.mjs +1012 -0
  94. package/dinou/node/dev.mjs +2589 -0
  95. package/dinou/node/port-selector.mjs +164 -0
  96. package/dinou/node/terminal-status.mjs +345 -0
  97. package/dinou/package.json +17 -14
  98. package/dinou/rollup/react-refresh/esm-hmr/client.js +66 -10
  99. package/dinou/rollup/react-refresh/esm-hmr/server.js +1 -1
  100. package/dinou/rollup/react-refresh/is-react-refresh-boundary.js +24 -5
  101. package/dinou/rollup/react-refresh/react-refresh-runtime.js +13 -6
  102. package/dinou/rollup/react-refresh/react-refresh-wrap-modules.js +56 -11
  103. package/dinou/rollup/react-refresh/rollup-plugin-esm-hmr.js +176 -48
  104. package/dinou/rollup/rollup-plugins/dinou-asset-plugin.js +9 -5
  105. package/dinou/rollup/rollup-plugins/rollup-plugin-memory.js +66 -0
  106. package/dinou/rollup/rollup-plugins/rollup-plugin-react-client-manifest.js +468 -123
  107. package/dinou/rollup/rollup-plugins/rollup-plugin-server-functions.js +44 -12
  108. package/dinou/rollup/rollup-plugins/rollup-plugin-swc.js +100 -0
  109. package/dinou/rollup/rollup.config.js +155 -43
  110. package/dinou/server.d.ts +114 -12
  111. package/dinou/server.js +35 -10
  112. package/dinou/server.mjs +30 -9
  113. package/dinou/webpack/helpers/get-webpack-entries.js +14 -19
  114. package/dinou/webpack/loaders/server-functions-loader.js +22 -1
  115. package/dinou/webpack/loaders/swc-loader.js +76 -0
  116. package/dinou/webpack/plugins/manifest-generator-plugin.js +1 -0
  117. package/dinou/webpack/plugins/server-functions-plugin.js +75 -19
  118. package/dinou/webpack/plugins/webpack-memory-plugin.js +66 -0
  119. package/dinou/webpack/webpack.config.js +227 -29
  120. package/eject.js +173 -94
  121. package/package.json +110 -107
  122. package/dinou/core/asset-require-hook.js +0 -58
  123. package/dinou/core/client-error-webpack.jsx +0 -432
  124. package/dinou/core/client-error.jsx +0 -432
  125. package/dinou/core/concurrency-manager.js +0 -48
  126. package/dinou/core/context-proxy.js +0 -68
  127. package/dinou/core/generate-static-pages.js +0 -130
  128. package/dinou/core/generate-static-rscs.js +0 -151
  129. package/dinou/core/get-ssg-metadata.js +0 -31
  130. package/dinou/core/register-loader.mjs +0 -11
  131. package/dinou/core/register-paths.js +0 -27
  132. package/dinou/core/render-app-to-html.js +0 -430
  133. package/dinou/core/render-html.js +0 -407
  134. package/dinou/core/server.js +0 -1542
  135. package/dinou/esbuild/plugins-esbuild/write-metafile-plugin.mjs +0 -16
@@ -96,35 +96,49 @@ class HotModuleState {
96
96
  }
97
97
 
98
98
  export function createHotContext(id) {
99
- const existing = REGISTERED_MODULES[id];
99
+ const normId = id.startsWith("/") ? id : "/" + id;
100
+ const existing = REGISTERED_MODULES[normId] || REGISTERED_MODULES[id];
100
101
  if (existing) {
101
- existing.lock();
102
+ existing.isLocked = false;
103
+ existing.acceptCallbacks = [];
102
104
  return existing;
103
105
  }
104
- const state = new HotModuleState(id);
106
+ const state = new HotModuleState(normId);
107
+ REGISTERED_MODULES[normId] = state;
105
108
  REGISTERED_MODULES[id] = state;
106
109
  return state;
107
110
  }
108
111
 
109
112
  async function applyUpdate(id) {
110
- const state = REGISTERED_MODULES[id];
113
+ const clean = id ? id.split("?")[0] : "";
114
+ const normId = clean.startsWith("/") ? clean : "/" + clean;
115
+ const state = REGISTERED_MODULES[normId] || REGISTERED_MODULES[clean] || REGISTERED_MODULES[id];
111
116
  if (!state || state.isDeclined) {
112
117
  return false;
113
118
  }
114
119
 
115
- const acceptCallbacks = state.acceptCallbacks;
116
- const disposeCallbacks = state.disposeCallbacks;
120
+ const acceptCallbacks = [...state.acceptCallbacks];
121
+ const disposeCallbacks = [...state.disposeCallbacks];
117
122
  state.disposeCallbacks = [];
118
123
  state.data = {};
119
124
 
120
- disposeCallbacks.forEach((callback) => callback());
125
+ disposeCallbacks.forEach((callback) => {
126
+ try {
127
+ callback();
128
+ } catch (e) {
129
+ console.warn("[ESM-HMR] dispose callback error:", e);
130
+ }
131
+ });
121
132
 
122
133
  const updateID = Date.now();
123
134
 
124
135
  for (const { deps, callback: acceptCallback } of acceptCallbacks) {
125
136
  const [module, ...depModules] = await Promise.all([
126
- import(`/${id}` + `?mtime=${updateID}`),
127
- ...deps.map((d) => import(`/${d}` + `?mtime=${updateID}`)),
137
+ import(normId + `?mtime=${updateID}`),
138
+ ...deps.map((d) => {
139
+ const depId = d.startsWith("/") ? d : "/" + d;
140
+ return import(depId + `?mtime=${updateID}`);
141
+ }),
128
142
  ]);
129
143
  acceptCallback({ module, deps: depModules });
130
144
  }
@@ -142,18 +156,60 @@ socket.addEventListener("message", ({ data: _data }) => {
142
156
  return;
143
157
  }
144
158
 
159
+ if (data.type === "rsc-update") {
160
+ if (typeof window !== "undefined" && typeof window.__DINOU_ROUTER_REFRESH__ === "function") {
161
+ window.__DINOU_ROUTER_REFRESH__();
162
+ return;
163
+ }
164
+ reload();
165
+ return;
166
+ }
167
+
168
+ if (
169
+ data.type === "style-update" ||
170
+ (data.type === "update" && data.url && (data.url.endsWith(".css") || data.url.includes("styles.css")))
171
+ ) {
172
+ const targetUrl = data.url ? data.url.split("?")[0] : "";
173
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
174
+ for (const link of links) {
175
+ const rawHref = link.getAttribute("href");
176
+ if (!rawHref) continue;
177
+ const cleanHref = rawHref.split("?")[0];
178
+ if (!targetUrl || cleanHref.endsWith(targetUrl) || targetUrl.endsWith(cleanHref) || cleanHref.includes("styles.css")) {
179
+ const nextUrl = new URL(link.href, window.location.origin);
180
+ nextUrl.searchParams.set("t", String(Date.now()));
181
+ link.href = nextUrl.href;
182
+ }
183
+ }
184
+ return;
185
+ }
186
+
145
187
  if (data.type !== "update") {
146
188
  return;
147
189
  }
148
190
 
191
+ const tRecv = Date.now();
192
+ const isDebug = typeof window !== "undefined" && Boolean(window.__DINOU_DEBUG__);
193
+
194
+ if (isDebug) {
195
+ console.log(`⏱️ [BROWSER HMR] Received update for ${data.url} at ${new Date().toTimeString().slice(0, 8)}.${String(Date.now() % 1000).padStart(3, '0')}`);
196
+ }
197
+
149
198
  applyUpdate(data.url)
150
199
  .then((ok) => {
151
200
  if (!ok) {
201
+ if (isDebug) console.warn(`⏱️ [BROWSER HMR] applyUpdate returned false, falling back to full reload!`);
152
202
  reload();
203
+ } else {
204
+ if (isDebug) {
205
+ console.log(`⏱️ [BROWSER HMR] Fast Refresh finished applying in ${Date.now() - tRecv}ms!`);
206
+ } else {
207
+ console.log(`[ESM-HMR] hot updated: ${data.url}`);
208
+ }
153
209
  }
154
210
  })
155
211
  .catch((err) => {
156
- console.error(err);
212
+ console.error(`[ESM-HMR] applyUpdate error:`, err);
157
213
  reload();
158
214
  });
159
215
  });
@@ -16,7 +16,7 @@ class EsmHmrEngine {
16
16
  this.dependencyTree = map;
17
17
  const wss = options.server
18
18
  ? new WebSocket.Server({ noServer: true })
19
- : new WebSocket.Server({ port: 3001 });
19
+ : new WebSocket.Server({ port: options.port || Number(process.env.HMR_PORT) || 3001 });
20
20
 
21
21
  if (options.server) {
22
22
  options.server.on("upgrade", (req, socket, head) => {
@@ -9,13 +9,32 @@ export function isReactRefreshBoundary(RefreshRuntime, moduleExports) {
9
9
  let hasExports = false;
10
10
  let areAllExportsComponents = true;
11
11
  for (const key in moduleExports) {
12
- if (key === "__esModule") continue;
12
+ if (
13
+ key === "__esModule" ||
14
+ key === "metadata" ||
15
+ key === "revalidate" ||
16
+ key === "dynamic" ||
17
+ key === "dynamicParams" ||
18
+ key === "fetchCache" ||
19
+ key === "preferredRegion" ||
20
+ key === "runtime" ||
21
+ key === "maxDuration" ||
22
+ key === "generateStaticParams" ||
23
+ key === "generateMetadata" ||
24
+ key === "frontmatter" ||
25
+ key === "toc" ||
26
+ key === "headings"
27
+ ) {
28
+ continue;
29
+ }
13
30
 
14
31
  hasExports = true;
15
- const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
16
- if (desc && desc.get) return false;
17
-
18
- const exportValue = moduleExports[key];
32
+ let exportValue;
33
+ try {
34
+ exportValue = moduleExports[key];
35
+ } catch (e) {
36
+ return false;
37
+ }
19
38
  if (!RefreshRuntime.isLikelyComponentType(exportValue)) {
20
39
  areAllExportsComponents = false;
21
40
  }
@@ -1,22 +1,29 @@
1
1
  import RefreshRuntime from "/refresh.js";
2
2
  import { isReactRefreshBoundary } from "./is-react-refresh-boundary";
3
3
 
4
+ const runtime = RefreshRuntime?.default || RefreshRuntime;
5
+
4
6
  if (
5
7
  typeof window !== "undefined" &&
6
8
  !window.__REACT_REFRESH_RUNTIME_INSTALLED__
7
9
  ) {
8
- RefreshRuntime.injectIntoGlobalHook(window);
9
- window.$RefreshReg$ = () => {};
10
- window.$RefreshSig$ = () => (type) => type;
10
+ runtime.injectIntoGlobalHook(window);
11
+ window.__reactRefreshRuntime = runtime;
12
+ window.$RefreshReg$ = (type, id) => {
13
+ runtime?.register(type, id);
14
+ };
15
+ window.$RefreshSig$ = runtime?.createSignatureFunctionForTransform
16
+ ? runtime.createSignatureFunctionForTransform.bind(runtime)
17
+ : () => (type) => type;
11
18
  window.__REACT_REFRESH_RUNTIME_INSTALLED__ = true;
12
19
 
13
20
  let refreshTimeout;
14
- window.performReactRefresh = RefreshRuntime.performReactRefresh;
21
+ window.performReactRefresh = runtime.performReactRefresh;
15
22
  window.__debouncePerformReactRefresh = () => {
16
23
  clearTimeout(refreshTimeout);
17
24
  refreshTimeout = setTimeout(() => {
18
25
  try {
19
- RefreshRuntime.performReactRefresh();
26
+ runtime.performReactRefresh();
20
27
  } catch (err) {
21
28
  console.warn("React Refresh failed:", err);
22
29
  }
@@ -24,5 +31,5 @@ if (
24
31
  };
25
32
 
26
33
  window.__isReactRefreshBoundary = (moduleExports) =>
27
- isReactRefreshBoundary(RefreshRuntime, moduleExports);
34
+ isReactRefreshBoundary(runtime, moduleExports);
28
35
  }
@@ -1,3 +1,15 @@
1
+ const frameworkEntries = new Set([
2
+ "main.js",
3
+ "runtime.js",
4
+ "refresh.js",
5
+ "serverFunctionProxy.js",
6
+ "dinouClientRedirect.js",
7
+ "dinouLink.js",
8
+ "dinouSlot.js",
9
+ "__hmr_client__.js",
10
+ "_commonjsHelpers.js",
11
+ ]);
12
+
1
13
  function reactRefreshWrapModules() {
2
14
  return {
3
15
  name: "react-refresh-wrap-modules",
@@ -5,35 +17,67 @@ function reactRefreshWrapModules() {
5
17
  if (
6
18
  !chunk ||
7
19
  !/\.(jsx?|tsx?)$/.test(chunk.fileName) ||
20
+ frameworkEntries.has(chunk.fileName) ||
8
21
  chunk.fileName.includes("refresh.js") ||
9
22
  chunk.fileName.includes("runtime.js") ||
10
- chunk.fileName.includes("_commonjsHelpers.js")
23
+ chunk.fileName.includes("_commonjsHelpers")
11
24
  ) {
12
25
  return null;
13
26
  }
14
- const safeId = JSON.stringify(chunk.fileName);
15
- const wrappedCode = `
16
- import RefreshRuntime from "/refresh.js";
17
27
 
28
+ // Only wrap user component chunks that contain actual app code (not third-party libraries)
29
+ const modulePaths = Object.keys(chunk.modules || {});
30
+ const hasUserCode = modulePaths.some(
31
+ (p) =>
32
+ !p.includes("node_modules") &&
33
+ !p.includes("dinou[\\/]core") &&
34
+ !p.includes("dinou[\\/]rollup") &&
35
+ !p.includes("dinou[\\/]esbuild") &&
36
+ !p.includes("dinou[\\/]webpack") &&
37
+ !p.includes("dinou/core") &&
38
+ !p.includes("dinou/rollup") &&
39
+ !p.includes("dinou/esbuild") &&
40
+ !p.includes("dinou/webpack")
41
+ );
42
+
43
+ if (!hasUserCode) {
44
+ return null;
45
+ }
46
+
47
+ const urlId = "/" + chunk.fileName;
48
+ const safeId = JSON.stringify(urlId);
49
+ const wrappedCode = `
50
+ const RefreshRuntime = window.__reactRefreshRuntime;
51
+ const actualRuntime = RefreshRuntime?.default || RefreshRuntime;
18
52
  let prevRefreshReg = window.$RefreshReg$;
19
53
  let prevRefreshSig = window.$RefreshSig$;
20
54
 
21
55
  window.$RefreshReg$ = (type, id) => {
22
- RefreshRuntime.register(type, ${safeId} + '#' + id);
56
+ const fullId = (id && id.includes('#')) ? id : (${safeId} + '#' + id);
57
+ actualRuntime?.register(type, fullId);
23
58
  };
24
- window.$RefreshSig$ = RefreshRuntime?.createSignatureFunctionForTransform;
59
+ window.$RefreshSig$ = (actualRuntime?.createSignatureFunctionForTransform)
60
+ ? actualRuntime.createSignatureFunctionForTransform.bind(actualRuntime)
61
+ : (prevRefreshSig || (() => (type) => type));
62
+
63
+ if (!import.meta.hot) import.meta.hot = window.__hotContext?.(${safeId});
25
64
 
26
65
  // --- original code ---
27
66
  ${code}
28
67
  // --- end original code ---
29
68
 
30
- window.$RefreshReg$ = prevRefreshReg;
31
- window.$RefreshSig$ = prevRefreshSig;
32
-
33
69
  if (import.meta.hot) {
34
- import.meta.hot.accept();
35
- window.__debouncePerformReactRefresh?.();
70
+ import.meta.hot.accept(({ module }) => {
71
+ if (window.__isReactRefreshBoundary && window.__isReactRefreshBoundary(module)) {
72
+ window.__debouncePerformReactRefresh?.();
73
+ } else {
74
+ import.meta.hot.invalidate();
75
+ }
76
+ });
36
77
  }
78
+
79
+ window.$RefreshReg$ = prevRefreshReg;
80
+ window.$RefreshSig$ = prevRefreshSig;
37
81
  `;
38
82
 
39
83
  return {
@@ -45,3 +89,4 @@ if (import.meta.hot) {
45
89
  }
46
90
 
47
91
  module.exports = reactRefreshWrapModules;
92
+
@@ -6,22 +6,40 @@ const { createServer } = require("node:http");
6
6
  const changedIds = new Set();
7
7
  const pendingUpdateUrls = new Set();
8
8
  let needsFullReload = false;
9
+ let hasPendingCssUpdate = false;
10
+
11
+ function normalizePath(p) {
12
+ return path.resolve(p).replace(/\\/g, "/").toLowerCase();
13
+ }
14
+
15
+ let activeServer = null;
16
+ let activeHmrEngine = null;
17
+
9
18
  function esmHmrPlugin() {
10
- let hmrEngine;
11
- let serverStarted = false;
19
+ let hmrEngine = activeHmrEngine;
12
20
 
13
21
  return {
14
22
  name: "esm-hmr",
15
23
 
16
24
  buildStart() {
17
- if (!serverStarted) {
18
- const server = createServer();
19
- hmrEngine = new EsmHmrEngine({ server });
20
- server.listen(3001, () => {
21
- // console.log("[esm-hmr] WebSocket server listening on port 3001");
25
+ if (!activeHmrEngine) {
26
+ const hmrPort = Number(
27
+ process.env.HMR_PORT || (Number(process.env.PORT || 3000) + 1)
28
+ );
29
+ activeServer = createServer();
30
+ activeServer.on("error", (err) => {
31
+ if (err.code === "EADDRINUSE") {
32
+ console.warn(`⚠️ [Rollup HMR] Port ${hmrPort} already in use, reusing existing listener.`);
33
+ } else {
34
+ console.error("❌ [Rollup HMR Server Error]:", err);
35
+ }
36
+ });
37
+ activeHmrEngine = new EsmHmrEngine({ server: activeServer });
38
+ activeServer.listen(hmrPort, () => {
39
+ // console.log(`[esm-hmr] WebSocket server listening on port ${hmrPort}`);
22
40
  });
23
- serverStarted = true;
24
41
  }
42
+ hmrEngine = activeHmrEngine;
25
43
  },
26
44
 
27
45
  renderChunk(code, chunk) {
@@ -34,84 +52,194 @@ function esmHmrPlugin() {
34
52
  return null;
35
53
  }
36
54
 
37
- const acceptsHmr = code.includes("import.meta.hot.accept");
38
55
  const imports = Array.from(code.matchAll(/import\s+["'](.+?)["']/g)).map(
39
56
  (m) => m[1]
40
57
  );
41
58
  const normalizedId = chunk.fileName;
42
- hmrEngine.setEntry(normalizedId, imports, true);
59
+ const urlId = "/" + chunk.fileName;
60
+ hmrEngine?.setEntry(urlId, imports, true);
61
+ hmrEngine?.setEntry(normalizedId, imports, true);
43
62
 
44
63
  const isClientEntry = normalizedId === "main.js";
45
64
 
46
- let injectCode = "";
47
-
48
- // 🔥 Inject client HMR runtime if it's the entry
65
+ // Inject client HMR runtime if it's the entry
49
66
  if (isClientEntry && !code.includes("/__hmr_client__.js")) {
50
- injectCode += `import { createHotContext } from "/__hmr_client__.js";window.__hotContext = createHotContext;\n`;
51
- }
52
-
53
- // 🔥 Inject import.meta.hot definition if user accepts HMR
54
- if (acceptsHmr) {
55
- const safeId = JSON.stringify(normalizedId);
56
- injectCode += `if (!import.meta.hot) import.meta.hot = window.__hotContext?.(${safeId});\n`;
57
- }
58
-
59
- if (injectCode) {
60
- code = injectCode + code;
67
+ return {
68
+ code: `import { createHotContext } from "/__hmr_client__.js";window.__hotContext = createHotContext;\n` + code,
69
+ map: null,
70
+ };
61
71
  }
62
72
 
63
- return {
64
- code,
65
- map: null,
66
- };
73
+ return null;
67
74
  },
68
75
 
69
76
  watchChange(id) {
70
- changedIds.add(id);
77
+ changedIds.add(normalizePath(id));
71
78
  },
72
79
 
73
80
  generateBundle(options, bundle) {
74
81
  const clientPath = path.resolve(__dirname, "./esm-hmr/client.js");
82
+ let clientSource = fs.readFileSync(clientPath, "utf-8");
83
+ const isDebug =
84
+ process.env.DINOU_DEBUG === "true" ||
85
+ process.env.DINOU_DEBUG === "1" ||
86
+ process.env.DEBUG === "true" ||
87
+ process.env.DEBUG === "1";
88
+ if (isDebug) {
89
+ clientSource = "window.__DINOU_DEBUG__ = true;\n" + clientSource;
90
+ }
75
91
  this.emitFile({
76
92
  type: "asset",
77
93
  fileName: "__hmr_client__.js",
78
- source: fs.readFileSync(clientPath, "utf-8"),
94
+ source: clientSource,
79
95
  });
80
- },
81
- writeBundle(_options, bundle) {
82
- for (const [fileName, chunkInfo] of Object.entries(bundle)) {
83
- for (const modulePath of Object.keys(chunkInfo.modules ?? {})) {
84
- if (changedIds.has(path.resolve(modulePath))) {
85
- const entry = hmrEngine.getEntry(fileName);
86
- if (entry?.isHmrAccepted) {
87
- pendingUpdateUrls.add(fileName);
88
- } else {
89
- needsFullReload = true;
96
+
97
+ if (changedIds.size > 0) {
98
+ hasPendingCssUpdate = Array.from(changedIds).some((id) => {
99
+ const lower = id.toLowerCase();
100
+ return lower.endsWith(".css") || lower.endsWith(".scss") || lower.endsWith(".less");
101
+ });
102
+
103
+ for (const [fileName, chunkInfo] of Object.entries(bundle)) {
104
+ if (fileName.endsWith(".css") || chunkInfo.type !== "chunk") {
105
+ continue;
106
+ }
107
+
108
+ let isChangedByJs = false;
109
+
110
+ // 1. Check facadeModuleId
111
+ if (chunkInfo.facadeModuleId) {
112
+ const facadeNorm = normalizePath(chunkInfo.facadeModuleId.replace(/\?.*$/, ""));
113
+ if (changedIds.has(facadeNorm)) {
114
+ isChangedByJs = true;
90
115
  }
91
116
  }
117
+
118
+ // 2. Check modules in chunk
119
+ if (!isChangedByJs && chunkInfo.modules) {
120
+ isChangedByJs = Object.keys(chunkInfo.modules).some((modPath) => {
121
+ const norm = normalizePath(modPath.replace(/\?.*$/, ""));
122
+ const lower = norm.toLowerCase();
123
+ if (
124
+ (lower.endsWith(".css") || lower.endsWith(".scss") || lower.endsWith(".less")) &&
125
+ !lower.endsWith(".module.css")
126
+ ) {
127
+ return false;
128
+ }
129
+ return changedIds.has(norm);
130
+ });
131
+ }
132
+
133
+ // 3. Fallback: match chunk filename with base name or relative path of changedId
134
+ if (!isChangedByJs) {
135
+ const chunkFileNameLower = fileName.toLowerCase().replace(/\.[jt]sx?$/, "");
136
+ for (const changedId of changedIds) {
137
+ const relClean = changedId
138
+ .replace(/^.*\/src\//, "")
139
+ .replace(/\.[jt]sx?$/, "")
140
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
141
+ .toLowerCase();
142
+ if (relClean && chunkFileNameLower.includes(relClean)) {
143
+ isChangedByJs = true;
144
+ break;
145
+ }
146
+ const baseName = path.basename(changedId).replace(/\.[jt]sx?$/, "").toLowerCase();
147
+ if (baseName.length > 2 && baseName !== "page" && baseName !== "layout" && baseName !== "index" && chunkFileNameLower.includes(baseName)) {
148
+ isChangedByJs = true;
149
+ break;
150
+ }
151
+ }
152
+ }
153
+
154
+ if (isChangedByJs) {
155
+ const urlId = "/" + fileName;
156
+ pendingUpdateUrls.add(urlId);
157
+ }
92
158
  }
93
159
  }
94
160
  },
95
- closeBundle() {
96
- if (changedIds.size === 0) return;
97
161
 
98
- if (needsFullReload || pendingUpdateUrls.size === 0) {
99
- // console.log("[HMR] Full reload");
100
- hmrEngine.broadcastMessage({ type: "reload" });
101
- } else {
162
+ writeBundle(_options, _bundle) {
163
+ if (changedIds.size === 0 && pendingUpdateUrls.size === 0 && !hasPendingCssUpdate) return;
164
+
165
+ if (hasPendingCssUpdate) {
166
+ hmrEngine?.broadcastMessage({ type: "style-update", url: "/styles.css" });
167
+ }
168
+
169
+ const timelineTime = () =>
170
+ new Date().toTimeString().slice(0, 8) +
171
+ "." +
172
+ String(Date.now() % 1000).padStart(3, "0");
173
+ const timelineRel = () =>
174
+ globalThis.__TIMELINE_T0__
175
+ ? `[+${Date.now() - globalThis.__TIMELINE_T0__}ms]`
176
+ : ``;
177
+
178
+ const isDebug =
179
+ process.env.DINOU_DEBUG === "true" ||
180
+ process.env.DINOU_DEBUG === "1" ||
181
+ process.env.DEBUG === "true" ||
182
+ process.env.DEBUG === "1";
183
+
184
+ if (isDebug && changedIds.size > 0) {
185
+ console.log(
186
+ `⏱️ [TIMELINE ${timelineTime()}] ${timelineRel()} [HMR Rollup] Changed: [${Array.from(changedIds).join(", ")}] -> Chunks: [${Array.from(pendingUpdateUrls).join(", ")}]`
187
+ );
188
+ }
189
+
190
+ if (pendingUpdateUrls.size > 0 && !needsFullReload) {
102
191
  for (const url of pendingUpdateUrls) {
103
- // console.log("[HMR] Broadcasting update for", url);
104
- hmrEngine.broadcastMessage({ type: "update", url });
192
+ if (isDebug) {
193
+ console.log(
194
+ `⏱️ [TIMELINE ${timelineTime()}] ${timelineRel()} ⚡ [HMR Broadcast] Sending update to browser: ${url}`
195
+ );
196
+ } else {
197
+ console.log(`⚡ [HMR] Updating client component: ${url}`);
198
+ }
199
+ hmrEngine?.broadcastMessage({ type: "update", url });
105
200
  }
201
+ } else if (needsFullReload) {
202
+ if (isDebug) {
203
+ console.log(
204
+ `⏱️ [TIMELINE ${timelineTime()}] ${timelineRel()} ⚡ [HMR Broadcast] Full reload triggered`
205
+ );
206
+ } else {
207
+ console.log(`⚡ [HMR] Full reload triggered`);
208
+ }
209
+ hmrEngine?.broadcastMessage({ type: "reload" });
106
210
  }
107
211
 
108
212
  changedIds.clear();
109
213
  pendingUpdateUrls.clear();
214
+ hasPendingCssUpdate = false;
110
215
  needsFullReload = false;
111
216
  },
217
+
218
+ closeWatcher() {
219
+ // Shared server stays open across restarts
220
+ },
112
221
  };
113
222
  }
114
223
 
224
+ function notifyFileChanged(absPath) {
225
+ if (absPath) {
226
+ changedIds.add(normalizePath(absPath));
227
+ }
228
+ }
229
+
115
230
  module.exports = {
116
231
  esmHmrPlugin,
232
+ getHmrEngine: () => activeHmrEngine,
233
+ notifyFileChanged,
234
+ closeHmrServer: () => {
235
+ if (activeServer) {
236
+ try {
237
+ activeServer.close();
238
+ } catch (e) {}
239
+ activeServer = null;
240
+ activeHmrEngine = null;
241
+ }
242
+ },
117
243
  };
244
+
245
+
@@ -17,11 +17,15 @@ function dinouAssetPlugin({ include = regex } = {}) {
17
17
 
18
18
  const fileName = `assets/${scoped}${ext}`;
19
19
 
20
- this.emitFile({
21
- type: "asset",
22
- fileName,
23
- source,
24
- });
20
+ const emitted = globalThis.__DINOU_EMITTED_ASSETS__ || (globalThis.__DINOU_EMITTED_ASSETS__ = new Set());
21
+ if (!emitted.has(fileName)) {
22
+ emitted.add(fileName);
23
+ this.emitFile({
24
+ type: "asset",
25
+ fileName,
26
+ source,
27
+ });
28
+ }
25
29
 
26
30
  return `export default '/assets/${scoped}${ext}';`;
27
31
  },
@@ -0,0 +1,66 @@
1
+ // dinou/rollup/rollup-plugins/rollup-plugin-memory.js
2
+ // Stores all bundled chunks, assets and sourcemaps in memory (globalThis.__DINOU_MEM_FILES__)
3
+ // and deletes them from Rollup's emit queue in dev mode to prevent massive disk writes.
4
+
5
+ const path = require("node:path");
6
+
7
+ function rollupMemoryPlugin() {
8
+ return {
9
+ name: "rollup-memory-plugin",
10
+ generateBundle(options, bundle) {
11
+ const isDev =
12
+ process.env.NODE_ENV !== "production" ||
13
+ process.env.DINOU_DEV === "true";
14
+
15
+ // Only acts in development mode. In production, Rollup writes to .dinou/dist3 normally.
16
+ if (!isDev) {
17
+ return;
18
+ }
19
+
20
+ const shouldWriteToDisk = process.env.DINOU_WRITE_TO_DISK === "true";
21
+
22
+ if (typeof globalThis !== "undefined") {
23
+ if (!globalThis.__DINOU_MEM_FILES__) {
24
+ globalThis.__DINOU_MEM_FILES__ = new Map();
25
+ }
26
+ }
27
+
28
+ for (const [fileName, item] of Object.entries(bundle)) {
29
+ const normKey = fileName.replace(/\\/g, "/");
30
+ const raw = item.type === "asset" ? item.source : item.code;
31
+ const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw || "");
32
+
33
+ if (typeof globalThis !== "undefined" && globalThis.__DINOU_MEM_FILES__) {
34
+ globalThis.__DINOU_MEM_FILES__.set(normKey, buf);
35
+ globalThis.__DINOU_MEM_FILES__.set("/" + normKey, buf);
36
+ const base = path.basename(normKey);
37
+ globalThis.__DINOU_MEM_FILES__.set(base, buf);
38
+ }
39
+
40
+ if (item.map) {
41
+ const mapKey = `${normKey}.map`;
42
+ const mapRaw =
43
+ typeof item.map.toString === "function"
44
+ ? item.map.toString()
45
+ : JSON.stringify(item.map);
46
+ const mapBuf = Buffer.from(mapRaw);
47
+ if (
48
+ typeof globalThis !== "undefined" &&
49
+ globalThis.__DINOU_MEM_FILES__
50
+ ) {
51
+ globalThis.__DINOU_MEM_FILES__.set(mapKey, mapBuf);
52
+ globalThis.__DINOU_MEM_FILES__.set("/" + mapKey, mapBuf);
53
+ const baseMap = path.basename(mapKey);
54
+ globalThis.__DINOU_MEM_FILES__.set(baseMap, mapBuf);
55
+ }
56
+ }
57
+
58
+ if (!shouldWriteToDisk) {
59
+ delete bundle[fileName];
60
+ }
61
+ }
62
+ },
63
+ };
64
+ }
65
+
66
+ module.exports = rollupMemoryPlugin;