@nitronjs/framework 0.2.27 → 0.3.1
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 +354 -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 +171 -23
- 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 +103 -1
- 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 +2 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +3 -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,33 +339,72 @@ 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
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
385
|
const entryLayoutSet = new Set(allFiles);
|
|
322
386
|
const changedSources = new Set();
|
|
323
387
|
|
|
324
|
-
|
|
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
|
+
}
|
|
396
|
+
|
|
325
397
|
const content = await fs.promises.readFile(file, "utf8");
|
|
326
398
|
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
327
399
|
const cachedHash = this.#cache.viewHashes.get(file);
|
|
328
400
|
|
|
401
|
+
this.#cache.viewHashes.set(mtimeKey, stat.mtimeMs);
|
|
402
|
+
|
|
329
403
|
if (cachedHash !== hash) {
|
|
330
404
|
this.#cache.viewHashes.set(file, hash);
|
|
331
405
|
changedSources.add(file);
|
|
332
406
|
}
|
|
333
|
-
}
|
|
407
|
+
}));
|
|
334
408
|
|
|
335
409
|
const filesToBuild = new Set();
|
|
336
410
|
|
|
@@ -350,7 +424,7 @@ class Builder {
|
|
|
350
424
|
|
|
351
425
|
if (changedFiles.length) {
|
|
352
426
|
this.#cache.viewsChanged = true;
|
|
353
|
-
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir });
|
|
427
|
+
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir, effectiveMap, clientPaths: clientPathSet });
|
|
354
428
|
await this.#postProcessMeta(changedFiles, srcDir, outDir);
|
|
355
429
|
|
|
356
430
|
for (const entry of changedFiles) {
|
|
@@ -361,7 +435,7 @@ class Builder {
|
|
|
361
435
|
|
|
362
436
|
this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
|
|
363
437
|
|
|
364
|
-
return { entries, layouts, meta, srcDir, namespace, changedFiles };
|
|
438
|
+
return { entries, layouts, meta, srcDir, namespace, changedFiles, changedSources };
|
|
365
439
|
}
|
|
366
440
|
|
|
367
441
|
async #postProcessMeta(entries, srcDir, outDir) {
|
|
@@ -437,36 +511,83 @@ class Builder {
|
|
|
437
511
|
await Promise.all(entries.map(processEntry));
|
|
438
512
|
}
|
|
439
513
|
|
|
440
|
-
async #
|
|
441
|
-
const
|
|
442
|
-
userBundle,
|
|
443
|
-
frameworkBundle,
|
|
444
|
-
this.#manifest,
|
|
445
|
-
changedViews
|
|
446
|
-
);
|
|
514
|
+
async #buildClientComponents(userBundle, frameworkBundle, changedViews = null) {
|
|
515
|
+
const clientFiles = [];
|
|
447
516
|
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
540
|
}
|
|
451
541
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
}));
|
|
559
|
+
|
|
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, "/");
|
|
460
571
|
|
|
461
|
-
|
|
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));
|
|
462
582
|
}
|
|
463
583
|
|
|
464
584
|
#getTransitiveDependents(file, importedBy) {
|
|
465
585
|
const result = new Set();
|
|
466
586
|
const queue = [file];
|
|
587
|
+
let i = 0;
|
|
467
588
|
|
|
468
|
-
while (queue.length
|
|
469
|
-
const current = queue
|
|
589
|
+
while (i < queue.length) {
|
|
590
|
+
const current = queue[i++];
|
|
470
591
|
const dependents = importedBy.get(current) || [];
|
|
471
592
|
|
|
472
593
|
for (const dep of dependents) {
|
|
@@ -481,18 +602,31 @@ class Builder {
|
|
|
481
602
|
}
|
|
482
603
|
|
|
483
604
|
async #buildCss() {
|
|
484
|
-
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged);
|
|
605
|
+
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged, this.#isDev);
|
|
485
606
|
}
|
|
486
607
|
|
|
487
608
|
async #runEsbuild(entries, outDir, options = {}) {
|
|
488
609
|
if (!entries.length) return;
|
|
489
610
|
|
|
611
|
+
const isNode = (options.platform ?? "node") === "node";
|
|
612
|
+
|
|
490
613
|
const plugins = [
|
|
491
614
|
createCssStubPlugin(),
|
|
492
|
-
createMarkerPlugin(options, this.#isDev),
|
|
493
615
|
createPathAliasPlugin()
|
|
494
616
|
];
|
|
495
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
|
+
|
|
496
630
|
if (options.vendor) {
|
|
497
631
|
plugins.push(createVendorGlobalsPlugin());
|
|
498
632
|
plugins.push(createServerModuleBlockerPlugin());
|
|
@@ -502,8 +636,6 @@ class Builder {
|
|
|
502
636
|
plugins.push(createServerFunctionsPlugin());
|
|
503
637
|
}
|
|
504
638
|
|
|
505
|
-
const isNode = (options.platform ?? "node") === "node";
|
|
506
|
-
|
|
507
639
|
const config = {
|
|
508
640
|
entryPoints: entries,
|
|
509
641
|
outdir: outDir,
|
|
@@ -535,7 +667,7 @@ class Builder {
|
|
|
535
667
|
await esbuild.build(config);
|
|
536
668
|
}
|
|
537
669
|
|
|
538
|
-
#addToManifest(entries, layouts, meta, baseDir, namespace) {
|
|
670
|
+
#addToManifest(entries, layouts, meta, baseDir, namespace, effectiveMap) {
|
|
539
671
|
const layoutSet = new Set(layouts);
|
|
540
672
|
|
|
541
673
|
for (const file of entries) {
|
|
@@ -567,11 +699,26 @@ class Builder {
|
|
|
567
699
|
cssSet.add(`/css/${path.basename(css)}`);
|
|
568
700
|
}
|
|
569
701
|
|
|
570
|
-
|
|
702
|
+
const entry = {
|
|
571
703
|
css: [...cssSet],
|
|
572
704
|
layouts: layoutChain.map(l => l.name),
|
|
573
705
|
hydrationScript: null
|
|
574
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;
|
|
575
722
|
}
|
|
576
723
|
|
|
577
724
|
for (const layout of layouts) {
|
|
@@ -595,15 +742,164 @@ class Builder {
|
|
|
595
742
|
}
|
|
596
743
|
}
|
|
597
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
|
+
|
|
598
880
|
#writeManifest() {
|
|
599
881
|
const manifestPath = path.join(Paths.build, "manifest.json");
|
|
882
|
+
const tmpPath = manifestPath + ".tmp";
|
|
883
|
+
const data = JSON.stringify(this.#manifest, null, 2);
|
|
884
|
+
|
|
600
885
|
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
601
|
-
fs.writeFileSync(
|
|
886
|
+
fs.writeFileSync(tmpPath, data);
|
|
887
|
+
|
|
888
|
+
try { fs.renameSync(tmpPath, manifestPath); }
|
|
889
|
+
catch {
|
|
890
|
+
fs.writeFileSync(manifestPath, data);
|
|
891
|
+
}
|
|
602
892
|
}
|
|
603
893
|
|
|
604
894
|
#writeJsxRuntime() {
|
|
605
|
-
|
|
606
|
-
|
|
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);
|
|
607
903
|
}
|
|
608
904
|
|
|
609
905
|
#cleanOutputDirs() {
|