@nitronjs/framework 0.2.26 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +2 -33
- package/lib/Build/Manager.js +390 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +177 -24
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +144 -1
- package/lib/Session/Redis.js +117 -0
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +3 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +4 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
package/lib/Build/Manager.js
CHANGED
|
@@ -10,17 +10,19 @@ import Layout from "../View/Layout.js";
|
|
|
10
10
|
import JSX_RUNTIME from "./jsxRuntime.js";
|
|
11
11
|
import FileAnalyzer from "./FileAnalyzer.js";
|
|
12
12
|
import CssBuilder from "./CssBuilder.js";
|
|
13
|
-
import HydrationBuilder from "./HydrationBuilder.js";
|
|
14
13
|
import {
|
|
15
14
|
createPathAliasPlugin,
|
|
16
15
|
createOriginalJsxPlugin,
|
|
17
16
|
createVendorGlobalsPlugin,
|
|
18
17
|
createServerFunctionsPlugin,
|
|
19
18
|
createCssStubPlugin,
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
createServerModuleBlockerPlugin,
|
|
20
|
+
createClientReferencePlugin,
|
|
21
|
+
createPropFilterPlugin
|
|
22
22
|
} from "./plugins.js";
|
|
23
23
|
import { transformFile as factoryTransform } from "./FactoryTransform.js";
|
|
24
|
+
import PropUsageAnalyzer from "./PropUsageAnalyzer.js";
|
|
25
|
+
import EffectivePropUsage from "./EffectivePropUsage.js";
|
|
24
26
|
import COLORS from "./colors.js";
|
|
25
27
|
|
|
26
28
|
dotenv.config({ quiet: true });
|
|
@@ -43,21 +45,21 @@ class Builder {
|
|
|
43
45
|
css: new Map(),
|
|
44
46
|
cssHashes: new Map(),
|
|
45
47
|
viewHashes: new Map(),
|
|
46
|
-
hydrationTemplate: null,
|
|
47
|
-
hydrationTemplateDev: null,
|
|
48
48
|
vendorBuilt: false,
|
|
49
49
|
spaBuilt: false,
|
|
50
50
|
hmrBuilt: false,
|
|
51
|
+
rscConsumerBuilt: false,
|
|
51
52
|
tailwindProcessor: null,
|
|
52
53
|
viewsChanged: false,
|
|
53
54
|
fileMeta: new Map(),
|
|
54
|
-
fileHashes: new Map()
|
|
55
|
+
fileHashes: new Map(),
|
|
56
|
+
propAnalysis: new Map()
|
|
55
57
|
};
|
|
56
58
|
#diskCachePath = path.join(Paths.nitronTemp, "build-cache.json");
|
|
57
59
|
#changedFiles = new Set();
|
|
60
|
+
#devBuiltOnce = false;
|
|
58
61
|
#analyzer;
|
|
59
62
|
#cssBuilder;
|
|
60
|
-
#hydrationBuilder;
|
|
61
63
|
|
|
62
64
|
constructor() {
|
|
63
65
|
this.#paths = {
|
|
@@ -74,8 +76,7 @@ class Builder {
|
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
this.#analyzer = new FileAnalyzer(this.#cache);
|
|
77
|
-
this.#cssBuilder = new CssBuilder(this.#cache, this.#
|
|
78
|
-
this.#hydrationBuilder = new HydrationBuilder(this.#cache, this.#isDev, this.#paths.templates, this.#analyzer);
|
|
79
|
+
this.#cssBuilder = new CssBuilder(this.#cache, this.#paths.cssInput, this.#paths.cssOutput);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -96,7 +97,7 @@ class Builder {
|
|
|
96
97
|
try {
|
|
97
98
|
if (this.#isDev) this.#loadDiskCache();
|
|
98
99
|
if (!silent) console.log(`\n${COLORS.cyan}⚡ NitronJS Build${COLORS.reset}\n`);
|
|
99
|
-
if (!only) this.#cleanOutputDirs();
|
|
100
|
+
if (!only && !(this.#isDev && this.#devBuiltOnce)) this.#cleanOutputDirs();
|
|
100
101
|
|
|
101
102
|
this.#cache.viewsChanged = false;
|
|
102
103
|
this.#writeJsxRuntime();
|
|
@@ -104,7 +105,10 @@ class Builder {
|
|
|
104
105
|
if (!only || only === "views") await this.#buildViews();
|
|
105
106
|
if (!only || only === "css") await this.#buildCss();
|
|
106
107
|
|
|
107
|
-
if (this.#isDev)
|
|
108
|
+
if (this.#isDev) {
|
|
109
|
+
this.#devBuiltOnce = true;
|
|
110
|
+
this.#saveDiskCache();
|
|
111
|
+
}
|
|
108
112
|
this.#cleanupTemp();
|
|
109
113
|
if (!silent) this.#printSummary(Date.now() - startTime);
|
|
110
114
|
|
|
@@ -116,7 +120,8 @@ class Builder {
|
|
|
116
120
|
time: Date.now() - startTime
|
|
117
121
|
};
|
|
118
122
|
} catch (error) {
|
|
119
|
-
this.#cleanupTemp();
|
|
123
|
+
try { this.#cleanupTemp(); }
|
|
124
|
+
catch {}
|
|
120
125
|
|
|
121
126
|
if (!silent) {
|
|
122
127
|
console.log(this.#formatBuildError(error));
|
|
@@ -133,8 +138,8 @@ class Builder {
|
|
|
133
138
|
|
|
134
139
|
if (data.fileMeta) {
|
|
135
140
|
for (const [key, value] of Object.entries(data.fileMeta)) {
|
|
136
|
-
value.imports =
|
|
137
|
-
value.css =
|
|
141
|
+
value.imports = value.imports || [];
|
|
142
|
+
value.css = value.css || [];
|
|
138
143
|
this.#cache.fileMeta.set(key, value);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
@@ -153,6 +158,11 @@ class Builder {
|
|
|
153
158
|
this.#cache.cssHashes.set(key, value);
|
|
154
159
|
}
|
|
155
160
|
}
|
|
161
|
+
if (data.propAnalysis) {
|
|
162
|
+
for (const [key, value] of Object.entries(data.propAnalysis)) {
|
|
163
|
+
this.#cache.propAnalysis.set(key, value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
156
166
|
}
|
|
157
167
|
} catch {}
|
|
158
168
|
}
|
|
@@ -168,10 +178,12 @@ class Builder {
|
|
|
168
178
|
fileMeta: {},
|
|
169
179
|
fileHashes: {},
|
|
170
180
|
viewHashes: {},
|
|
171
|
-
cssHashes: {}
|
|
181
|
+
cssHashes: {},
|
|
182
|
+
propAnalysis: {}
|
|
172
183
|
};
|
|
173
184
|
|
|
174
185
|
for (const [key, value] of this.#cache.fileMeta) {
|
|
186
|
+
if (!fs.existsSync(key)) continue;
|
|
175
187
|
data.fileMeta[key] = {
|
|
176
188
|
...value,
|
|
177
189
|
imports: Array.from(value.imports || []),
|
|
@@ -179,16 +191,30 @@ class Builder {
|
|
|
179
191
|
};
|
|
180
192
|
}
|
|
181
193
|
for (const [key, value] of this.#cache.fileHashes) {
|
|
194
|
+
if (!fs.existsSync(key)) continue;
|
|
182
195
|
data.fileHashes[key] = value;
|
|
183
196
|
}
|
|
184
197
|
for (const [key, value] of this.#cache.viewHashes) {
|
|
198
|
+
const filePath = key.endsWith(":mtime") ? key.slice(0, -6) : key;
|
|
199
|
+
if (!fs.existsSync(filePath)) continue;
|
|
185
200
|
data.viewHashes[key] = value;
|
|
186
201
|
}
|
|
187
202
|
for (const [key, value] of this.#cache.cssHashes) {
|
|
203
|
+
if (!fs.existsSync(key)) continue;
|
|
188
204
|
data.cssHashes[key] = value;
|
|
189
205
|
}
|
|
206
|
+
for (const [key, value] of this.#cache.propAnalysis) {
|
|
207
|
+
if (!fs.existsSync(key)) continue;
|
|
208
|
+
data.propAnalysis[key] = value;
|
|
209
|
+
}
|
|
190
210
|
|
|
191
|
-
|
|
211
|
+
const tmpPath = this.#diskCachePath + ".tmp";
|
|
212
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data));
|
|
213
|
+
|
|
214
|
+
try { fs.renameSync(tmpPath, this.#diskCachePath); }
|
|
215
|
+
catch {
|
|
216
|
+
fs.writeFileSync(this.#diskCachePath, JSON.stringify(data));
|
|
217
|
+
}
|
|
192
218
|
} catch {}
|
|
193
219
|
}
|
|
194
220
|
|
|
@@ -218,10 +244,11 @@ class Builder {
|
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
async #buildViews() {
|
|
221
|
-
const [, , , userBundle, frameworkBundle] = await Promise.all([
|
|
247
|
+
const [, , , , userBundle, frameworkBundle] = await Promise.all([
|
|
222
248
|
this.#buildVendor(),
|
|
223
249
|
this.#buildSpaRuntime(),
|
|
224
250
|
this.#buildHmrClient(),
|
|
251
|
+
this.#buildRscConsumer(),
|
|
225
252
|
this.#buildViewBundle("user", this.#paths.userViews, this.#paths.userOutput),
|
|
226
253
|
this.#buildViewBundle("framework", this.#paths.frameworkViews, this.#paths.frameworkOutput)
|
|
227
254
|
]);
|
|
@@ -231,19 +258,27 @@ class Builder {
|
|
|
231
258
|
...(frameworkBundle.changedFiles || [])
|
|
232
259
|
]);
|
|
233
260
|
|
|
261
|
+
// Include changed client component sources so their hydration bundles rebuild
|
|
262
|
+
for (const bundle of [userBundle, frameworkBundle]) {
|
|
263
|
+
if (!bundle?.changedSources) continue;
|
|
264
|
+
|
|
265
|
+
for (const file of bundle.changedSources) {
|
|
266
|
+
const fileMeta = bundle.meta?.get(file);
|
|
267
|
+
if (fileMeta?.isClient) changedViews.add(file);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
234
271
|
for (const file of changedViews) {
|
|
235
272
|
this.#changedFiles.add(file);
|
|
236
273
|
}
|
|
237
274
|
|
|
238
|
-
const
|
|
239
|
-
(userBundle.entries.length > 0 || frameworkBundle.entries.length > 0);
|
|
240
|
-
|
|
241
|
-
await this.#buildHydrationBundles(
|
|
275
|
+
const clientComponents = await this.#buildClientComponents(
|
|
242
276
|
userBundle,
|
|
243
277
|
frameworkBundle,
|
|
244
|
-
|
|
278
|
+
changedViews
|
|
245
279
|
);
|
|
246
280
|
|
|
281
|
+
this.#writeClientManifest(clientComponents);
|
|
247
282
|
this.#writeManifest();
|
|
248
283
|
}
|
|
249
284
|
|
|
@@ -304,36 +339,92 @@ class Builder {
|
|
|
304
339
|
this.#cache.spaBuilt = true;
|
|
305
340
|
}
|
|
306
341
|
|
|
342
|
+
async #buildRscConsumer() {
|
|
343
|
+
const outfile = path.join(this.#paths.jsOutput, "rsc-consumer.js");
|
|
344
|
+
if (this.#cache.rscConsumerBuilt && fs.existsSync(outfile)) return;
|
|
345
|
+
|
|
346
|
+
const consumerFile = path.join(this.#paths.templates, "rsc-consumer.tsx");
|
|
347
|
+
|
|
348
|
+
await esbuild.build({
|
|
349
|
+
entryPoints: [consumerFile],
|
|
350
|
+
outfile,
|
|
351
|
+
bundle: true,
|
|
352
|
+
platform: "browser",
|
|
353
|
+
format: "iife",
|
|
354
|
+
target: "es2020",
|
|
355
|
+
sourcemap: this.#isDev,
|
|
356
|
+
minify: !this.#isDev,
|
|
357
|
+
keepNames: true,
|
|
358
|
+
jsx: "automatic",
|
|
359
|
+
plugins: [
|
|
360
|
+
createVendorGlobalsPlugin(),
|
|
361
|
+
createCssStubPlugin()
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
this.#cache.rscConsumerBuilt = true;
|
|
365
|
+
}
|
|
366
|
+
|
|
307
367
|
async #buildViewBundle(namespace, srcDir, outDir) {
|
|
308
368
|
if (!fs.existsSync(srcDir)) {
|
|
309
|
-
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
369
|
+
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
|
|
310
370
|
}
|
|
311
371
|
|
|
312
|
-
const { entries, layouts, meta } = this.#analyzer.discoverEntries(srcDir);
|
|
372
|
+
const { entries, layouts, meta, importedBy } = this.#analyzer.discoverEntries(srcDir);
|
|
313
373
|
|
|
314
374
|
if (!entries.length && !layouts.length) {
|
|
315
|
-
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
375
|
+
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
|
|
316
376
|
}
|
|
317
377
|
|
|
318
|
-
|
|
378
|
+
// Pre-scan: compute effective prop usage for server→client boundary filtering.
|
|
379
|
+
// Must run before addToManifest so manifest gets effective (forward-aware) usage.
|
|
380
|
+
const { effectiveMap, clientPathSet } = this.#preScanPropUsage(meta);
|
|
381
|
+
|
|
382
|
+
this.#addToManifest(entries, layouts, meta, srcDir, namespace, effectiveMap);
|
|
319
383
|
|
|
320
384
|
const allFiles = [...entries, ...layouts];
|
|
321
|
-
const
|
|
385
|
+
const entryLayoutSet = new Set(allFiles);
|
|
386
|
+
const changedSources = new Set();
|
|
387
|
+
|
|
388
|
+
await Promise.all([...meta.keys()].map(async (file) => {
|
|
389
|
+
const stat = await fs.promises.stat(file);
|
|
390
|
+
const mtimeKey = file + ":mtime";
|
|
391
|
+
const cachedMtime = this.#cache.viewHashes.get(mtimeKey);
|
|
392
|
+
|
|
393
|
+
if (cachedMtime === stat.mtimeMs && this.#cache.viewHashes.has(file)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
322
396
|
|
|
323
|
-
for (const file of allFiles) {
|
|
324
397
|
const content = await fs.promises.readFile(file, "utf8");
|
|
325
398
|
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
326
399
|
const cachedHash = this.#cache.viewHashes.get(file);
|
|
327
400
|
|
|
401
|
+
this.#cache.viewHashes.set(mtimeKey, stat.mtimeMs);
|
|
402
|
+
|
|
328
403
|
if (cachedHash !== hash) {
|
|
329
404
|
this.#cache.viewHashes.set(file, hash);
|
|
330
|
-
|
|
405
|
+
changedSources.add(file);
|
|
406
|
+
}
|
|
407
|
+
}));
|
|
408
|
+
|
|
409
|
+
const filesToBuild = new Set();
|
|
410
|
+
|
|
411
|
+
for (const file of changedSources) {
|
|
412
|
+
if (entryLayoutSet.has(file)) {
|
|
413
|
+
filesToBuild.add(file);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const dep of this.#getTransitiveDependents(file, importedBy)) {
|
|
417
|
+
if (entryLayoutSet.has(dep)) {
|
|
418
|
+
filesToBuild.add(dep);
|
|
419
|
+
}
|
|
331
420
|
}
|
|
332
421
|
}
|
|
333
422
|
|
|
423
|
+
const changedFiles = [...filesToBuild];
|
|
424
|
+
|
|
334
425
|
if (changedFiles.length) {
|
|
335
426
|
this.#cache.viewsChanged = true;
|
|
336
|
-
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir });
|
|
427
|
+
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir, effectiveMap, clientPaths: clientPathSet });
|
|
337
428
|
await this.#postProcessMeta(changedFiles, srcDir, outDir);
|
|
338
429
|
|
|
339
430
|
for (const entry of changedFiles) {
|
|
@@ -344,7 +435,7 @@ class Builder {
|
|
|
344
435
|
|
|
345
436
|
this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
|
|
346
437
|
|
|
347
|
-
return { entries, layouts, meta, srcDir, namespace, changedFiles };
|
|
438
|
+
return { entries, layouts, meta, srcDir, namespace, changedFiles, changedSources };
|
|
348
439
|
}
|
|
349
440
|
|
|
350
441
|
async #postProcessMeta(entries, srcDir, outDir) {
|
|
@@ -420,43 +511,122 @@ class Builder {
|
|
|
420
511
|
await Promise.all(entries.map(processEntry));
|
|
421
512
|
}
|
|
422
513
|
|
|
423
|
-
async #
|
|
424
|
-
const
|
|
425
|
-
userBundle,
|
|
426
|
-
frameworkBundle,
|
|
427
|
-
this.#manifest,
|
|
428
|
-
changedViews
|
|
429
|
-
);
|
|
514
|
+
async #buildClientComponents(userBundle, frameworkBundle, changedViews = null) {
|
|
515
|
+
const clientFiles = [];
|
|
430
516
|
|
|
431
|
-
|
|
432
|
-
|
|
517
|
+
for (const bundle of [userBundle, frameworkBundle]) {
|
|
518
|
+
if (!bundle?.meta) continue;
|
|
519
|
+
|
|
520
|
+
for (const [filePath, fileMeta] of bundle.meta.entries()) {
|
|
521
|
+
if (!fileMeta.isClient) continue;
|
|
522
|
+
|
|
523
|
+
const relative = path.relative(bundle.srcDir, filePath)
|
|
524
|
+
.replace(/\.tsx$/, ".js")
|
|
525
|
+
.replace(/\\/g, "/");
|
|
526
|
+
|
|
527
|
+
const chunkFile = "js/" + relative;
|
|
528
|
+
const outputPath = path.join(this.#paths.jsOutput, relative);
|
|
529
|
+
const needsRebuild = !changedViews || changedViews.has(filePath);
|
|
530
|
+
|
|
531
|
+
clientFiles.push({
|
|
532
|
+
sourcePath: filePath,
|
|
533
|
+
srcDir: bundle.srcDir,
|
|
534
|
+
chunkId: relative.replace(/\.js$/, ""),
|
|
535
|
+
chunkFile,
|
|
536
|
+
outputPath,
|
|
537
|
+
needsRebuild
|
|
538
|
+
});
|
|
539
|
+
}
|
|
433
540
|
}
|
|
434
541
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
542
|
+
const toBuild = clientFiles.filter(c => c.needsRebuild || !fs.existsSync(c.outputPath));
|
|
543
|
+
|
|
544
|
+
if (toBuild.length) {
|
|
545
|
+
const srcDirs = new Set(toBuild.map(c => c.srcDir));
|
|
546
|
+
|
|
547
|
+
await Promise.all([...srcDirs].map(srcDir => {
|
|
548
|
+
const entries = toBuild.filter(c => c.srcDir === srcDir).map(c => c.sourcePath);
|
|
549
|
+
|
|
550
|
+
return this.#runEsbuild(entries, this.#paths.jsOutput, {
|
|
551
|
+
platform: "browser",
|
|
552
|
+
target: "es2020",
|
|
553
|
+
outbase: srcDir,
|
|
554
|
+
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"],
|
|
555
|
+
vendor: true,
|
|
556
|
+
serverFunctions: true
|
|
557
|
+
});
|
|
558
|
+
}));
|
|
443
559
|
|
|
444
|
-
|
|
560
|
+
this.#stats.islands = toBuild.length;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return clientFiles;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
#writeClientManifest(clientComponents) {
|
|
567
|
+
const manifest = {};
|
|
568
|
+
|
|
569
|
+
for (const { sourcePath, chunkId, chunkFile } of clientComponents) {
|
|
570
|
+
const fileUrl = "file:///" + sourcePath.replace(/\\/g, "/");
|
|
571
|
+
|
|
572
|
+
manifest[fileUrl] = {
|
|
573
|
+
id: chunkId,
|
|
574
|
+
chunks: [chunkId, chunkFile],
|
|
575
|
+
name: "*"
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const manifestPath = path.join(Paths.build, "client-manifest.json");
|
|
580
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
581
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
#getTransitiveDependents(file, importedBy) {
|
|
585
|
+
const result = new Set();
|
|
586
|
+
const queue = [file];
|
|
587
|
+
let i = 0;
|
|
588
|
+
|
|
589
|
+
while (i < queue.length) {
|
|
590
|
+
const current = queue[i++];
|
|
591
|
+
const dependents = importedBy.get(current) || [];
|
|
592
|
+
|
|
593
|
+
for (const dep of dependents) {
|
|
594
|
+
if (!result.has(dep)) {
|
|
595
|
+
result.add(dep);
|
|
596
|
+
queue.push(dep);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return result;
|
|
445
602
|
}
|
|
446
603
|
|
|
447
604
|
async #buildCss() {
|
|
448
|
-
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged);
|
|
605
|
+
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged, this.#isDev);
|
|
449
606
|
}
|
|
450
607
|
|
|
451
608
|
async #runEsbuild(entries, outDir, options = {}) {
|
|
452
609
|
if (!entries.length) return;
|
|
453
610
|
|
|
611
|
+
const isNode = (options.platform ?? "node") === "node";
|
|
612
|
+
|
|
454
613
|
const plugins = [
|
|
455
614
|
createCssStubPlugin(),
|
|
456
|
-
createMarkerPlugin(options, this.#isDev),
|
|
457
615
|
createPathAliasPlugin()
|
|
458
616
|
];
|
|
459
617
|
|
|
618
|
+
// RSC plugin only for server builds — replaces "use client" with reference stubs.
|
|
619
|
+
// Browser builds need the actual component code.
|
|
620
|
+
if (isNode) {
|
|
621
|
+
// Prop filter must be registered BEFORE client reference so its onLoad
|
|
622
|
+
// handles "nitron-filtered" namespace before createClientReferencePlugin tries.
|
|
623
|
+
if (options.effectiveMap && options.clientPaths) {
|
|
624
|
+
plugins.push(createPropFilterPlugin(options.effectiveMap, options.clientPaths));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
plugins.push(createClientReferencePlugin(this.#isDev));
|
|
628
|
+
}
|
|
629
|
+
|
|
460
630
|
if (options.vendor) {
|
|
461
631
|
plugins.push(createVendorGlobalsPlugin());
|
|
462
632
|
plugins.push(createServerModuleBlockerPlugin());
|
|
@@ -466,8 +636,6 @@ class Builder {
|
|
|
466
636
|
plugins.push(createServerFunctionsPlugin());
|
|
467
637
|
}
|
|
468
638
|
|
|
469
|
-
const isNode = (options.platform ?? "node") === "node";
|
|
470
|
-
|
|
471
639
|
const config = {
|
|
472
640
|
entryPoints: entries,
|
|
473
641
|
outdir: outDir,
|
|
@@ -499,7 +667,7 @@ class Builder {
|
|
|
499
667
|
await esbuild.build(config);
|
|
500
668
|
}
|
|
501
669
|
|
|
502
|
-
#addToManifest(entries, layouts, meta, baseDir, namespace) {
|
|
670
|
+
#addToManifest(entries, layouts, meta, baseDir, namespace, effectiveMap) {
|
|
503
671
|
const layoutSet = new Set(layouts);
|
|
504
672
|
|
|
505
673
|
for (const file of entries) {
|
|
@@ -531,11 +699,26 @@ class Builder {
|
|
|
531
699
|
cssSet.add(`/css/${path.basename(css)}`);
|
|
532
700
|
}
|
|
533
701
|
|
|
534
|
-
|
|
702
|
+
const entry = {
|
|
535
703
|
css: [...cssSet],
|
|
536
704
|
layouts: layoutChain.map(l => l.name),
|
|
537
705
|
hydrationScript: null
|
|
538
706
|
};
|
|
707
|
+
|
|
708
|
+
if (fileMeta?.isClient) {
|
|
709
|
+
const propUsage = effectiveMap.get(file) ?? null;
|
|
710
|
+
entry.propUsage = propUsage;
|
|
711
|
+
|
|
712
|
+
if (this.#isDev && propUsage === null) {
|
|
713
|
+
const rel = path.relative(Paths.project, file).replace(/\\/g, "/");
|
|
714
|
+
console.warn(`${COLORS.yellow}⚠ PropFilter: ${rel} — spread or dynamic prop access detected, all props will pass through${COLORS.reset}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Collect translation keys from view + all transitive imports
|
|
719
|
+
entry.translationKeys = this.#collectTranslationKeys(file, meta);
|
|
720
|
+
|
|
721
|
+
this.#manifest[key] = entry;
|
|
539
722
|
}
|
|
540
723
|
|
|
541
724
|
for (const layout of layouts) {
|
|
@@ -559,15 +742,164 @@ class Builder {
|
|
|
559
742
|
}
|
|
560
743
|
}
|
|
561
744
|
|
|
745
|
+
/**
|
|
746
|
+
* Collects translation keys from a view file and all its transitive imports.
|
|
747
|
+
*
|
|
748
|
+
* Walks the import graph starting from `file`, collecting every static translation
|
|
749
|
+
* key found via build-time AST analysis (PropUsageAnalyzer).
|
|
750
|
+
*
|
|
751
|
+
* Returns null (= "send ALL translations") when any file in the graph uses dynamic
|
|
752
|
+
* keys that can't be statically determined — e.g. `t(variable)` instead of `t("key")`.
|
|
753
|
+
*
|
|
754
|
+
* @param {string} file - Entry file path (absolute).
|
|
755
|
+
* @param {Map} meta - File metadata from FileAnalyzer.
|
|
756
|
+
* @returns {string[]|null} Array of keys, or null if any file has dynamic keys.
|
|
757
|
+
*/
|
|
758
|
+
#collectTranslationKeys(file, meta) {
|
|
759
|
+
const visited = new Set();
|
|
760
|
+
const allKeys = new Set();
|
|
761
|
+
let hasNull = false;
|
|
762
|
+
|
|
763
|
+
const walk = (filePath) => {
|
|
764
|
+
if (visited.has(filePath)) return;
|
|
765
|
+
|
|
766
|
+
visited.add(filePath);
|
|
767
|
+
|
|
768
|
+
const cached = this.#cache.propAnalysis.get(filePath);
|
|
769
|
+
|
|
770
|
+
if (!cached) {
|
|
771
|
+
// Non-JSX files (.ts, .js) don't have translation analysis — skip silently.
|
|
772
|
+
// JSX files without cache mean analysis failed — poison to null (send all).
|
|
773
|
+
if (!filePath.endsWith(".tsx") && !filePath.endsWith(".jsx")) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
hasNull = true;
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const keys = cached.translationKeys;
|
|
781
|
+
|
|
782
|
+
// null means "dynamic keys detected" — can't filter, must send all translations
|
|
783
|
+
if (keys === null) {
|
|
784
|
+
hasNull = true;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (keys) {
|
|
789
|
+
for (const k of keys) allKeys.add(k);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Walk transitive imports — resolve relative paths to absolute before lookup
|
|
793
|
+
const fileMeta = meta.get(filePath);
|
|
794
|
+
|
|
795
|
+
if (fileMeta?.imports) {
|
|
796
|
+
for (const imp of fileMeta.imports) {
|
|
797
|
+
const resolved = this.#analyzer.resolveImport(filePath, imp);
|
|
798
|
+
walk(resolved);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
walk(file);
|
|
804
|
+
|
|
805
|
+
if (hasNull) return null;
|
|
806
|
+
if (allKeys.size === 0) return [];
|
|
807
|
+
|
|
808
|
+
return [...allKeys];
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Pre-scan phase: analyzes JSX prop forwarding and computes effective prop usage.
|
|
813
|
+
* This enables the prop filter plugin to strip unused data at server→client boundaries.
|
|
814
|
+
* @param {Map} meta - File metadata from FileAnalyzer.
|
|
815
|
+
* @returns {{effectiveMap: Map, clientPathSet: Set}}
|
|
816
|
+
*/
|
|
817
|
+
#preScanPropUsage(meta) {
|
|
818
|
+
const propUsageMap = new Map();
|
|
819
|
+
const forwardMap = new Map();
|
|
820
|
+
const clientPathSet = new Set();
|
|
821
|
+
const cache = this.#cache.propAnalysis;
|
|
822
|
+
|
|
823
|
+
for (const [filePath, fileMeta] of meta.entries()) {
|
|
824
|
+
const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
825
|
+
|
|
826
|
+
if (!fileMeta.isClient && !isJsx) continue;
|
|
827
|
+
|
|
828
|
+
if (fileMeta.isClient) clientPathSet.add(filePath);
|
|
829
|
+
|
|
830
|
+
let stat;
|
|
831
|
+
|
|
832
|
+
try { stat = fs.statSync(filePath); }
|
|
833
|
+
catch { continue; }
|
|
834
|
+
|
|
835
|
+
const cached = cache.get(filePath);
|
|
836
|
+
|
|
837
|
+
// Fast path: mtime unchanged → skip entirely
|
|
838
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
839
|
+
if (fileMeta.isClient) {
|
|
840
|
+
propUsageMap.set(filePath, cached.propUsage);
|
|
841
|
+
forwardMap.set(filePath, cached.forwarding);
|
|
842
|
+
}
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// mtime changed — verify with content hash to avoid stale cache
|
|
847
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
848
|
+
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
849
|
+
|
|
850
|
+
if (cached && cached.hash === hash) {
|
|
851
|
+
const updatedCache = { ...cached, mtime: stat.mtimeMs };
|
|
852
|
+
cache.set(filePath, updatedCache);
|
|
853
|
+
|
|
854
|
+
if (fileMeta.isClient) {
|
|
855
|
+
propUsageMap.set(filePath, cached.propUsage);
|
|
856
|
+
forwardMap.set(filePath, cached.forwarding);
|
|
857
|
+
}
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const { propUsage, forwarding, translationKeys } = PropUsageAnalyzer.analyzeAll(filePath);
|
|
862
|
+
|
|
863
|
+
cache.set(filePath, { mtime: stat.mtimeMs, hash, propUsage, forwarding, translationKeys });
|
|
864
|
+
|
|
865
|
+
if (fileMeta.isClient) {
|
|
866
|
+
propUsageMap.set(filePath, propUsage);
|
|
867
|
+
forwardMap.set(filePath, forwarding);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (clientPathSet.size === 0) {
|
|
872
|
+
return { effectiveMap: new Map(), clientPathSet };
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const effectiveMap = EffectivePropUsage.compute(propUsageMap, forwardMap, clientPathSet);
|
|
876
|
+
|
|
877
|
+
return { effectiveMap, clientPathSet };
|
|
878
|
+
}
|
|
879
|
+
|
|
562
880
|
#writeManifest() {
|
|
563
881
|
const manifestPath = path.join(Paths.build, "manifest.json");
|
|
882
|
+
const tmpPath = manifestPath + ".tmp";
|
|
883
|
+
const data = JSON.stringify(this.#manifest, null, 2);
|
|
884
|
+
|
|
564
885
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
565
|
-
fs.writeFileSync(
|
|
886
|
+
fs.writeFileSync(tmpPath, data);
|
|
887
|
+
|
|
888
|
+
try { fs.renameSync(tmpPath, manifestPath); }
|
|
889
|
+
catch {
|
|
890
|
+
fs.writeFileSync(manifestPath, data);
|
|
891
|
+
}
|
|
566
892
|
}
|
|
567
893
|
|
|
568
894
|
#writeJsxRuntime() {
|
|
569
|
-
|
|
570
|
-
|
|
895
|
+
const runtimePath = this.#paths.jsxRuntime;
|
|
896
|
+
|
|
897
|
+
if (fs.existsSync(runtimePath) && fs.readFileSync(runtimePath, "utf8") === JSX_RUNTIME) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
fs.mkdirSync(path.dirname(runtimePath), { recursive: true });
|
|
902
|
+
fs.writeFileSync(runtimePath, JSX_RUNTIME);
|
|
571
903
|
}
|
|
572
904
|
|
|
573
905
|
#cleanOutputDirs() {
|