dinou 6.1.1 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +9352 -1081
  3. package/cli.js +314 -91
  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 +322 -165
  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 +175 -96
  39. package/dinou/core/handler.js +2303 -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 +183 -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 +252 -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 +91 -87
  53. package/dinou/core/request-context.js +47 -13
  54. package/dinou/core/resolve-module-ppr.js +34 -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 +16 -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 +113 -34
  121. package/package.json +18 -16
  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
@@ -4,10 +4,23 @@ import createScopedName from "../../core/createScopedName.js";
4
4
  import { regex } from "../../core/asset-extensions.js";
5
5
  import { getAbsPathWithExt } from "../../core/get-abs-path-with-ext.js";
6
6
  import { pathToFileURL } from "node:url";
7
+ import stripNamespace from "../helpers-esbuild/strip-namespace.mjs";
7
8
 
8
9
  const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
10
 
10
- export default function assetsPlugin({ include = regex } = {}) {
11
+ const normKey = (p) => {
12
+ if (!p) return "";
13
+ let s = path.resolve(p).replace(/\\/g, "/");
14
+ if (process.platform === "win32") {
15
+ s = s.replace(/^([a-zA-Z]):/, (_, d) => d.toLowerCase() + ":");
16
+ }
17
+ return s;
18
+ };
19
+
20
+ export default function assetsPlugin({ include = regex, changedIds } = {}) {
21
+ let isInitial = true;
22
+ let cachedRenames = new Map();
23
+
11
24
  return {
12
25
  name: "assets-plugin",
13
26
  setup(build) {
@@ -19,16 +32,30 @@ export default function assetsPlugin({ include = regex } = {}) {
19
32
  build.initialOptions.assetNames = "assets/[name]-[hash]";
20
33
 
21
34
  // Handle asset loading with different namespaces
22
- build.onResolve({ filter: include }, (args) => {
23
- // console.log("[assets-plugin] onResolve", args);
24
- const resolvedAlias =
35
+ build.onResolve({ filter: include }, async (args) => {
36
+ if (args.pluginData?.inAssetPlugin) return null;
37
+ let resolvedAlias =
25
38
  args.kind === "entry-point"
26
39
  ? args.path
27
40
  : getAbsPathWithExt(args.path, {
28
41
  parentURL: pathToFileURL(args.importer).href,
29
42
  });
30
43
 
31
- // CRUCIAL: Different namespace for entry points
44
+ if (!resolvedAlias) {
45
+ try {
46
+ const res = await build.resolve(args.path, {
47
+ resolveDir: args.resolveDir,
48
+ kind: args.kind,
49
+ pluginData: { inAssetPlugin: true },
50
+ });
51
+ if (res && res.path) {
52
+ resolvedAlias = res.path;
53
+ }
54
+ } catch (e) {}
55
+ }
56
+
57
+ if (!resolvedAlias) return null;
58
+
32
59
  if (args.kind === "entry-point") {
33
60
  return {
34
61
  path: resolvedAlias,
@@ -42,199 +69,247 @@ export default function assetsPlugin({ include = regex } = {}) {
42
69
  };
43
70
  });
44
71
 
72
+ // Cache asset file buffers in memory
73
+ const assetCache = new Map();
74
+ const emittedAssets = new Map();
75
+
45
76
  // Loader for normal assets
46
77
  build.onLoad({ filter: /.*/, namespace: "dinou-asset" }, async (args) => {
47
- const contents = await fs.readFile(args.path);
48
- return { contents, loader: "file" };
78
+ let contents = assetCache.get(args.path);
79
+ if (!contents) {
80
+ contents = await fs.readFile(args.path);
81
+ assetCache.set(args.path, contents);
82
+ }
83
+ const ext = path.extname(args.path);
84
+ const base = path.basename(args.path, ext);
85
+ const scoped = createScopedName(base, args.path);
86
+ const assetUrl = `/assets/${scoped}${ext}`;
87
+ const newLocal = `assets/${scoped}${ext}`;
88
+
89
+ emittedAssets.set(newLocal, {
90
+ path: path.join(outdir, newLocal),
91
+ contents,
92
+ get text() {
93
+ return new TextDecoder().decode(this.contents);
94
+ },
95
+ });
96
+
97
+ return {
98
+ contents: `export default "${assetUrl}";`,
99
+ loader: "js",
100
+ watchFiles: [args.path],
101
+ };
49
102
  });
50
103
 
51
104
  // Loader for asset entry points
52
105
  build.onLoad(
53
106
  { filter: /.*/, namespace: "dinou-asset-entry" },
54
107
  async (args) => {
55
- const contents = await fs.readFile(args.path);
56
- return { contents, loader: "file" };
108
+ let contents = assetCache.get(args.path);
109
+ if (!contents) {
110
+ contents = await fs.readFile(args.path);
111
+ assetCache.set(args.path, contents);
112
+ }
113
+ return { contents, loader: "file", watchFiles: [args.path] };
57
114
  }
58
115
  );
59
116
 
60
117
  build.onEnd(async (result) => {
61
- if (!result.metafile || !result.outputFiles?.length) return;
62
-
63
- const renames = new Map();
64
- const normalizeRel = (p) => p.replace(/\\/g, "/");
65
- const processedSourceFiles = new Set();
66
-
67
- // First pass: normal assets (individual files)
68
- for (const [oldRelPath, info] of Object.entries(
69
- result.metafile.outputs
70
- )) {
71
- if (info.entryPoint || Object.keys(info.inputs).length !== 1)
72
- continue;
73
-
74
- const inputPath = Object.keys(info.inputs)[0];
75
- const sourceFile = inputPath
76
- .replace(/^dinou-asset:/, "")
77
- .replace(/^dinou-asset-entry:/, "");
78
- if (!include.test(sourceFile)) continue;
79
-
80
- const ext = path.extname(sourceFile);
81
- if (!oldRelPath.endsWith(ext)) continue;
82
- const base = path.basename(sourceFile, ext);
83
- const scoped = createScopedName(base, sourceFile);
84
- const newLocal = `assets/${scoped}${ext}`;
85
- const oldLocal = normalizeRel(path.relative(outdir, oldRelPath));
86
-
87
- renames.set(oldLocal, newLocal);
88
- processedSourceFiles.add(sourceFile);
89
- }
118
+ const tAssets0 = Date.now();
119
+ try {
120
+ if (!result.metafile || !result.outputFiles?.length) return;
121
+
122
+ const normalizeRel = (p) => p.replace(/\\/g, "/");
123
+ const isIncremental = !isInitial && changedIds && changedIds.size > 0;
124
+ const hasAssetChanged = isIncremental && Array.from(changedIds).some((id) => include.test(id));
125
+
126
+ // Ensure JS-loaded assets are included in output files
127
+ for (const asset of emittedAssets.values()) {
128
+ if (!result.outputFiles.some((f) => f.path === asset.path)) {
129
+ result.outputFiles.push(asset);
130
+ }
131
+ }
90
132
 
91
- // Second pass: find assets that are inside chunks
92
- for (const [outputPath, info] of Object.entries(
93
- result.metafile.outputs
94
- )) {
95
- // Only search in JavaScript chunks
96
- if (!outputPath.endsWith(".js") || info.entryPoint) continue;
133
+ let renames = cachedRenames;
97
134
 
98
- for (const inputPath of Object.keys(info.inputs)) {
99
- if (!inputPath.startsWith("dinou-asset:")) continue;
135
+ // Only re-scan all asset outputs on initial build or if an asset file actually changed
136
+ if (isInitial || hasAssetChanged) {
137
+ renames = new Map();
138
+ const processedSourceFiles = new Set();
100
139
 
101
- const sourceFile = inputPath.replace(/^dinou-asset:/, "");
102
- if (
103
- !include.test(sourceFile) ||
104
- processedSourceFiles.has(sourceFile)
105
- )
106
- continue;
140
+ // First pass: normal assets (individual files)
141
+ for (const [oldRelPath, info] of Object.entries(
142
+ result.metafile.outputs
143
+ )) {
144
+ if (info.entryPoint || Object.keys(info.inputs).length !== 1)
145
+ continue;
146
+
147
+ const inputPath = Object.keys(info.inputs)[0];
148
+ const sourceFile = inputPath
149
+ .replace(/^dinou-asset:/, "")
150
+ .replace(/^dinou-asset-entry:/, "");
151
+ if (!include.test(sourceFile)) continue;
107
152
 
108
- try {
109
153
  const ext = path.extname(sourceFile);
154
+ if (!oldRelPath.endsWith(ext)) continue;
110
155
  const base = path.basename(sourceFile, ext);
111
156
  const scoped = createScopedName(base, sourceFile);
112
157
  const newLocal = `assets/${scoped}${ext}`;
158
+ const oldLocal = normalizeRel(path.relative(outdir, oldRelPath));
113
159
 
114
- // Read the original asset
115
- const assetContent = await fs.readFile(sourceFile);
116
-
117
- // Create a new output file for this asset
118
- const newOutputFile = {
119
- path: path.join(outdir, newLocal),
120
- contents: assetContent,
121
- get text() {
122
- return new TextDecoder().decode(this.contents);
123
- },
124
- };
125
-
126
- result.outputFiles.push(newOutputFile);
160
+ renames.set(oldLocal, newLocal);
127
161
  processedSourceFiles.add(sourceFile);
162
+ }
128
163
 
129
- // console.log(
130
- // `Extracted asset from chunk: ${sourceFile} → ${newLocal}`
131
- // );
132
-
133
- // Find the chunk that contains this asset
134
- const chunkFile = result.outputFiles.find(
135
- (f) =>
136
- normalizeRel(path.relative(process.cwd(), f.path)) ===
137
- outputPath
138
- );
139
-
140
- if (chunkFile) {
141
- let chunkContent = new TextDecoder().decode(chunkFile.contents);
142
-
143
- // MORE PRECISE APPROACH: Find the specific comment for this asset
144
- const assetComment = `// ${inputPath}`;
145
- const commentIndex = chunkContent.indexOf(assetComment);
146
-
147
- if (commentIndex !== -1) {
148
- // Find the next line containing the variable assignment
149
- const nextLineStart =
150
- chunkContent.indexOf("\n", commentIndex) + 1;
151
- const nextLineEnd = chunkContent.indexOf("\n", nextLineStart);
152
- const assignmentLine = chunkContent.substring(
153
- nextLineStart,
154
- nextLineEnd
155
- );
164
+ // Pre-index outputFiles for O(1) lookup
165
+ const outputFilesByRelPath = new Map();
166
+ for (const f of result.outputFiles) {
167
+ outputFilesByRelPath.set(normalizeRel(path.relative(process.cwd(), f.path)), f);
168
+ }
156
169
 
157
- // Extract the variable name (e.g., "dinou_default")
158
- const varMatch = assignmentLine.match(
159
- /var (\w+)_default = "([^"]+)"/
170
+ // Second pass: find assets that are inside chunks
171
+ for (const [outputPath, info] of Object.entries(
172
+ result.metafile.outputs
173
+ )) {
174
+ if (!outputPath.endsWith(".js") || info.entryPoint) continue;
175
+
176
+ for (const inputPath of Object.keys(info.inputs)) {
177
+ if (!inputPath.startsWith("dinou-asset:")) continue;
178
+
179
+ const sourceFile = inputPath.replace(/^dinou-asset:/, "");
180
+ if (
181
+ !include.test(sourceFile) ||
182
+ processedSourceFiles.has(sourceFile)
183
+ )
184
+ continue;
185
+
186
+ try {
187
+ const ext = path.extname(sourceFile);
188
+ const base = path.basename(sourceFile, ext);
189
+ const scoped = createScopedName(base, sourceFile);
190
+ const newLocal = `assets/${scoped}${ext}`;
191
+
192
+ const assetContent = await fs.readFile(sourceFile);
193
+
194
+ const newOutputFile = {
195
+ path: path.join(outdir, newLocal),
196
+ contents: assetContent,
197
+ get text() {
198
+ return new TextDecoder().decode(this.contents);
199
+ },
200
+ };
201
+
202
+ result.outputFiles.push(newOutputFile);
203
+ processedSourceFiles.add(sourceFile);
204
+
205
+ const chunkFile = outputFilesByRelPath.get(outputPath);
206
+
207
+ if (chunkFile) {
208
+ let chunkContent = new TextDecoder().decode(chunkFile.contents);
209
+ const assetComment = `// ${inputPath}`;
210
+ const commentIndex = chunkContent.indexOf(assetComment);
211
+
212
+ if (commentIndex !== -1) {
213
+ const nextLineStart = chunkContent.indexOf("\n", commentIndex) + 1;
214
+ const nextLineEnd = chunkContent.indexOf("\n", nextLineStart);
215
+ const assignmentLine = chunkContent.substring(nextLineStart, nextLineEnd);
216
+ const varMatch = assignmentLine.match(/var (\w+)_default = "([^"]+)"/);
217
+
218
+ if (varMatch) {
219
+ const varName = varMatch[1];
220
+ const newAssignmentLine = `var ${varName}_default = "/${newLocal}";`;
221
+ chunkContent =
222
+ chunkContent.substring(0, nextLineStart) +
223
+ newAssignmentLine +
224
+ chunkContent.substring(nextLineEnd);
225
+ }
226
+ } else {
227
+ const assetName = path.basename(sourceFile);
228
+ const escapedAssetName = escapeRegExp(assetName);
229
+ const pattern = new RegExp(
230
+ `(var \\w+_default = ")([^"]*${escapedAssetName}[^"]*)(";)`,
231
+ "g"
232
+ );
233
+
234
+ if (pattern.test(chunkContent)) {
235
+ chunkContent = chunkContent.replace(
236
+ pattern,
237
+ `$1/${newLocal}$3`
238
+ );
239
+ }
240
+ }
241
+
242
+ chunkFile.contents = new TextEncoder().encode(chunkContent);
243
+ }
244
+ } catch (error) {
245
+ console.error(
246
+ `Error extracting asset ${sourceFile} from chunk:`,
247
+ error
160
248
  );
249
+ }
250
+ }
251
+ }
161
252
 
162
- if (varMatch) {
163
- const varName = varMatch[1];
164
- const oldPath = varMatch[2];
253
+ cachedRenames = renames;
254
+ }
165
255
 
166
- // Replace ONLY this specific assignment
167
- const newAssignmentLine = `var ${varName}_default = "/${newLocal}";`;
168
- chunkContent =
169
- chunkContent.substring(0, nextLineStart) +
170
- newAssignmentLine +
171
- chunkContent.substring(nextLineEnd);
256
+ if (renames.size === 0) return;
172
257
 
173
- // console.log(
174
- // `Updated reference in chunk: ${oldPath} → /${newLocal}`
175
- // );
176
- }
177
- } else {
178
- // Fallback: search by filename in the path
179
- const assetName = path.basename(sourceFile);
180
- const escapedAssetName = escapeRegExp(assetName);
181
- const pattern = new RegExp(
182
- `(var \\w+_default = ")([^"]*${escapedAssetName}[^"]*)(";)`,
183
- "g"
184
- );
258
+ // Update references in JS/CSS files:
259
+ // On incremental rebuild, only process output files belonging to changed modules!
260
+ for (const file of result.outputFiles) {
261
+ const relPath = normalizeRel(path.relative(process.cwd(), file.path));
262
+ if (!relPath.endsWith(".js") && !relPath.endsWith(".css")) continue;
185
263
 
186
- if (pattern.test(chunkContent)) {
187
- chunkContent = chunkContent.replace(
188
- pattern,
189
- `$1/${newLocal}$3`
190
- );
191
- }
192
- }
264
+ if (isIncremental && !hasAssetChanged) {
265
+ const outputInfo = result.metafile.outputs[relPath];
266
+ const inputFiles = Object.keys(outputInfo?.inputs || {});
267
+ const touchesChanged = inputFiles.some((m) => {
268
+ const clean = stripNamespace(m);
269
+ return changedIds.has(normKey(clean));
270
+ });
271
+ if (!touchesChanged) continue;
272
+ }
193
273
 
194
- chunkFile.contents = new TextEncoder().encode(chunkContent);
274
+ let content = new TextDecoder().decode(file.contents);
275
+ let modified = false;
276
+ for (const [oldLocal, newLocal] of renames) {
277
+ if (!content.includes(oldLocal)) continue;
278
+ const patterns = [
279
+ [`"./${escapeRegExp(oldLocal)}"`, `"/${newLocal}"`],
280
+ [`"${escapeRegExp(oldLocal)}"`, `"${newLocal}"`],
281
+ [`'./${escapeRegExp(oldLocal)}'`, `'/${newLocal}'`],
282
+ [`'${escapeRegExp(oldLocal)}'`, `'${newLocal}'`],
283
+ ];
284
+
285
+ for (const [oldPattern, newPattern] of patterns) {
286
+ const reg = new RegExp(oldPattern, "g");
287
+ if (reg.test(content)) {
288
+ content = content.replace(reg, newPattern);
289
+ modified = true;
290
+ }
195
291
  }
196
- } catch (error) {
197
- console.error(
198
- `Error extracting asset ${sourceFile} from chunk:`,
199
- error
200
- );
201
292
  }
202
- }
203
- }
204
-
205
- // Update references in JS/CSS files (existing code)
206
- for (const file of result.outputFiles) {
207
- const relPath = normalizeRel(path.relative(process.cwd(), file.path));
208
- if (!relPath.endsWith(".js") && !relPath.endsWith(".css")) continue;
209
-
210
- let content = new TextDecoder().decode(file.contents);
211
- for (const [oldLocal, newLocal] of renames) {
212
- const patterns = [
213
- [`"./${escapeRegExp(oldLocal)}"`, `"/${newLocal}"`],
214
- [`"${escapeRegExp(oldLocal)}"`, `"${newLocal}"`],
215
- [`'./${escapeRegExp(oldLocal)}'`, `'/${newLocal}'`],
216
- [`'${escapeRegExp(oldLocal)}'`, `'${newLocal}'`],
217
- ];
218
-
219
- for (const [oldPattern, newPattern] of patterns) {
220
- content = content.replace(
221
- new RegExp(oldPattern, "g"),
222
- newPattern
223
- );
293
+ if (modified) {
294
+ file.contents = new TextEncoder().encode(content);
224
295
  }
225
296
  }
226
- file.contents = new TextEncoder().encode(content);
227
- }
228
297
 
229
- // Update paths (existing code)
230
- for (const file of result.outputFiles) {
231
- const relPath = normalizeRel(path.relative(process.cwd(), file.path));
232
- const oldLocal = normalizeRel(path.relative(outdir, relPath));
233
- const newLocal = renames.get(oldLocal);
298
+ // Update paths in outputFiles
299
+ if (!isIncremental) {
300
+ for (const file of result.outputFiles) {
301
+ const relPath = normalizeRel(path.relative(process.cwd(), file.path));
302
+ const oldLocal = normalizeRel(path.relative(outdir, relPath));
303
+ const newLocal = renames.get(oldLocal);
234
304
 
235
- if (newLocal) {
236
- file.path = path.join(outdir, newLocal);
305
+ if (newLocal) {
306
+ file.path = path.join(outdir, newLocal);
307
+ }
308
+ }
237
309
  }
310
+ } finally {
311
+ isInitial = false;
312
+ globalThis.__DINOU_ASSETS_TIME__ = Date.now() - tAssets0;
238
313
  }
239
314
  });
240
315
  },
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs/promises";
2
+ import fsSync from "node:fs";
2
3
  import path from "node:path";
3
4
  import tailwindcss from "@tailwindcss/postcss";
4
5
  import autoprefixer from "autoprefixer";
@@ -11,19 +12,94 @@ import { pathToFileURL } from "node:url";
11
12
  import resolve from "resolve";
12
13
  import createPostCSSExtractPlugin from "../plugins-postcss/postcss-extract-plugin.js";
13
14
 
14
- export default function cssProcessorPlugin({ outdir = ".dinou/public" } = {}) {
15
- const { finalize, plugin: extractor } = createPostCSSExtractPlugin({
15
+ const cssCache = new Map();
16
+ let cssCacheLoaded = false;
17
+ let cssCacheDirty = false;
18
+
19
+ function getCssCachePath() {
20
+ const dir = path.resolve(process.cwd(), ".dinou/cache");
21
+ return { dir, file: path.join(dir, "css-dev-cache.json") };
22
+ }
23
+
24
+ function loadCssCache() {
25
+ if (cssCacheLoaded) return;
26
+ cssCacheLoaded = true;
27
+ try {
28
+ const { file } = getCssCachePath();
29
+ if (fsSync.existsSync(file)) {
30
+ const parsed = JSON.parse(fsSync.readFileSync(file, "utf8"));
31
+ for (const [k, v] of Object.entries(parsed)) {
32
+ cssCache.set(k, v);
33
+ }
34
+ }
35
+ } catch (e) {}
36
+ }
37
+
38
+ async function saveCssCache() {
39
+ if (!cssCacheDirty) return;
40
+ cssCacheDirty = false;
41
+ try {
42
+ const { dir, file } = getCssCachePath();
43
+ if (!fsSync.existsSync(dir)) {
44
+ fsSync.mkdirSync(dir, { recursive: true });
45
+ }
46
+ const data = {};
47
+ for (const [k, v] of cssCache.entries()) {
48
+ data[k] = v;
49
+ }
50
+ await fs.writeFile(file, JSON.stringify(data), "utf8");
51
+ } catch (e) {}
52
+ }
53
+
54
+ export default function cssProcessorPlugin({ outdir = ".dinou/public", hmrEngine } = {}) {
55
+ const { finalize, plugin: extractor, addExtractedCss } = createPostCSSExtractPlugin({
16
56
  outputFile: `${outdir}/styles.css`,
17
57
  });
18
58
 
59
+ let isInitial = true;
60
+ let hasCssChange = false;
61
+ let postCssTotalTime = 0;
62
+ let postCssCount = 0;
63
+
19
64
  return {
20
65
  name: "css-processor",
21
66
  setup(build) {
67
+ build.onStart(() => {
68
+ hasCssChange = false;
69
+ postCssTotalTime = 0;
70
+ postCssCount = 0;
71
+ });
72
+
22
73
  build.onLoad({ filter: /\.css$/ }, async (args) => {
23
74
  const filePath = args.path;
75
+ loadCssCache();
76
+ const stat = await fs.stat(filePath);
77
+ const cached = cssCache.get(filePath);
78
+
79
+ const isModule = filePath.endsWith(".module.css");
80
+ if (isModule && cached && cached.mtime === stat.mtimeMs) {
81
+ if (cached.extractedCss && typeof addExtractedCss === "function") {
82
+ addExtractedCss(cached.extractedCss);
83
+ }
84
+ return {
85
+ contents: `export default ${JSON.stringify(cached.map || {})};`,
86
+ loader: "js",
87
+ watchFiles: [filePath],
88
+ };
89
+ }
90
+
91
+ hasCssChange = true;
92
+ const tPostCss0 = Date.now();
24
93
  const source = await fs.readFile(filePath, "utf8");
25
94
 
26
95
  let map = {};
96
+ let extractedForFile = "";
97
+ const fileCapturePlugin = {
98
+ postcssPlugin: "file-capture",
99
+ OnceExit(root) {
100
+ extractedForFile = root.toString();
101
+ },
102
+ };
27
103
 
28
104
  await postcss([
29
105
  postcssImport({
@@ -32,11 +108,9 @@ export default function cssProcessorPlugin({ outdir = ".dinou/public" } = {}) {
32
108
  parentURL: pathToFileURL(basedir).href,
33
109
  });
34
110
  if (resolvedAlias) {
35
- console.log("ALIAS RESOLVED:", resolvedAlias);
36
111
  return resolvedAlias;
37
112
  }
38
113
  if (id.startsWith("tailwindcss/")) {
39
- console.log("TAILWIND INTERNAL:", id);
40
114
  return resolve.sync(id, { basedir, extensions: [".css"] });
41
115
  }
42
116
  try {
@@ -58,24 +132,46 @@ export default function cssProcessorPlugin({ outdir = ".dinou/public" } = {}) {
58
132
  map = json;
59
133
  },
60
134
  }),
135
+ fileCapturePlugin,
61
136
  extractor,
62
137
  ]).process(source, { from: filePath });
63
138
 
139
+ postCssTotalTime += Date.now() - tPostCss0;
140
+ postCssCount++;
141
+ globalThis.__DINOU_POSTCSS_TIME__ = postCssTotalTime;
142
+ globalThis.__DINOU_POSTCSS_COUNT__ = postCssCount;
143
+
144
+ cssCache.set(filePath, {
145
+ mtime: stat.mtimeMs,
146
+ map,
147
+ extractedCss: extractedForFile,
148
+ });
149
+ cssCacheDirty = true;
150
+
64
151
  if (filePath.endsWith(".module.css")) {
65
- // console.log(`[CSS MODULE] ${path.basename(filePath)} →`, map);
66
152
  return {
67
153
  contents: `export default ${JSON.stringify(map)};`,
68
154
  loader: "js",
155
+ watchFiles: [filePath],
69
156
  };
70
157
  } else {
71
158
  return {
72
159
  contents: `/* global: ${path.basename(filePath)} */`,
73
160
  loader: "js",
161
+ watchFiles: [filePath],
74
162
  };
75
163
  }
76
164
  });
77
165
  build.onEnd(() => {
78
166
  finalize();
167
+ saveCssCache().catch(() => {});
168
+ globalThis.__DINOU_POSTCSS_TIME__ = postCssTotalTime;
169
+ globalThis.__DINOU_POSTCSS_COUNT__ = postCssCount;
170
+ if (!isInitial && hasCssChange && hmrEngine?.value?.broadcastMessage) {
171
+ hmrEngine.value.broadcastMessage({ type: "style-update", url: "/styles.css" });
172
+ }
173
+ isInitial = false;
174
+ hasCssChange = false;
79
175
  });
80
176
  },
81
177
  };