@nitronjs/framework 0.3.3 → 0.3.5

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.
@@ -18,11 +18,13 @@ import {
18
18
  createCssStubPlugin,
19
19
  createServerModuleBlockerPlugin,
20
20
  createClientReferencePlugin,
21
- createPropFilterPlugin
21
+ createPropFilterPlugin,
22
+ createFastRefreshPlugin
22
23
  } from "./plugins.js";
23
24
  import { transformFile as factoryTransform } from "./FactoryTransform.js";
24
25
  import PropUsageAnalyzer from "./PropUsageAnalyzer.js";
25
26
  import EffectivePropUsage from "./EffectivePropUsage.js";
27
+ import ModuleGraph from "./ModuleGraph.js";
26
28
  import COLORS from "./colors.js";
27
29
 
28
30
  dotenv.config({ quiet: true });
@@ -49,7 +51,7 @@ class Builder {
49
51
  spaBuilt: false,
50
52
  hmrBuilt: false,
51
53
  rscConsumerBuilt: false,
52
- tailwindProcessor: null,
54
+ refreshBuilt: false,
53
55
  viewsChanged: false,
54
56
  fileMeta: new Map(),
55
57
  fileHashes: new Map(),
@@ -60,6 +62,8 @@ class Builder {
60
62
  #devBuiltOnce = false;
61
63
  #analyzer;
62
64
  #cssBuilder;
65
+ #contexts = new Map();
66
+ #moduleGraph = new ModuleGraph();
63
67
 
64
68
  constructor() {
65
69
  this.#paths = {
@@ -117,6 +121,8 @@ class Builder {
117
121
  changedFiles: [...this.#changedFiles],
118
122
  cssChanged: this.#stats.css > 0,
119
123
  viewsChanged: this.#cache.viewsChanged,
124
+ rebuiltChunks: this.#cache.lastRebuiltChunks || [],
125
+ moduleGraph: this.#moduleGraph,
120
126
  time: Date.now() - startTime
121
127
  };
122
128
  } catch (error) {
@@ -133,38 +139,55 @@ class Builder {
133
139
 
134
140
  #loadDiskCache() {
135
141
  try {
136
- if (fs.existsSync(this.#diskCachePath)) {
137
- const data = JSON.parse(fs.readFileSync(this.#diskCachePath, "utf8"));
138
-
139
- if (data.fileMeta) {
140
- for (const [key, value] of Object.entries(data.fileMeta)) {
141
- value.imports = value.imports || [];
142
- value.css = value.css || [];
143
- this.#cache.fileMeta.set(key, value);
144
- }
142
+ if (!fs.existsSync(this.#diskCachePath)) return;
143
+
144
+ const data = JSON.parse(fs.readFileSync(this.#diskCachePath, "utf8"));
145
+
146
+ if (data.version !== this.#getCacheVersion()) {
147
+ fs.unlinkSync(this.#diskCachePath);
148
+ return;
149
+ }
150
+
151
+ if (data.fileMeta) {
152
+ for (const [key, value] of Object.entries(data.fileMeta)) {
153
+ value.imports = value.imports || [];
154
+ value.css = value.css || [];
155
+ this.#cache.fileMeta.set(key, value);
145
156
  }
146
- if (data.fileHashes) {
147
- for (const [key, value] of Object.entries(data.fileHashes)) {
148
- this.#cache.fileHashes.set(key, value);
149
- }
157
+ }
158
+
159
+ if (data.fileHashes) {
160
+ for (const [key, value] of Object.entries(data.fileHashes)) {
161
+ this.#cache.fileHashes.set(key, value);
150
162
  }
151
- if (data.viewHashes) {
152
- for (const [key, value] of Object.entries(data.viewHashes)) {
153
- this.#cache.viewHashes.set(key, value);
154
- }
163
+ }
164
+
165
+ if (data.viewHashes) {
166
+ for (const [key, value] of Object.entries(data.viewHashes)) {
167
+ this.#cache.viewHashes.set(key, value);
155
168
  }
156
- if (data.cssHashes) {
157
- for (const [key, value] of Object.entries(data.cssHashes)) {
158
- this.#cache.cssHashes.set(key, value);
159
- }
169
+ }
170
+
171
+ if (data.cssHashes) {
172
+ for (const [key, value] of Object.entries(data.cssHashes)) {
173
+ this.#cache.cssHashes.set(key, value);
160
174
  }
161
- if (data.propAnalysis) {
162
- for (const [key, value] of Object.entries(data.propAnalysis)) {
163
- this.#cache.propAnalysis.set(key, value);
164
- }
175
+ }
176
+
177
+ if (data.propAnalysis) {
178
+ for (const [key, value] of Object.entries(data.propAnalysis)) {
179
+ this.#cache.propAnalysis.set(key, value);
165
180
  }
166
181
  }
167
- } catch {}
182
+ }
183
+ catch {}
184
+ }
185
+
186
+ #getCacheVersion() {
187
+ const frameworkVersion = "0.3.3";
188
+ const esbuildVersion = esbuild.version || "unknown";
189
+
190
+ return frameworkVersion + ":" + esbuildVersion;
168
191
  }
169
192
 
170
193
  #saveDiskCache() {
@@ -175,6 +198,7 @@ class Builder {
175
198
  }
176
199
 
177
200
  const data = {
201
+ version: this.#getCacheVersion(),
178
202
  fileMeta: {},
179
203
  fileHashes: {},
180
204
  viewHashes: {},
@@ -244,10 +268,11 @@ class Builder {
244
268
  }
245
269
 
246
270
  async #buildViews() {
247
- const [, , , , userBundle, frameworkBundle] = await Promise.all([
271
+ const [, , , , , userBundle, frameworkBundle] = await Promise.all([
248
272
  this.#buildVendor(),
249
273
  this.#buildSpaRuntime(),
250
274
  this.#buildHmrClient(),
275
+ this.#buildRefreshRuntime(),
251
276
  this.#buildRscConsumer(),
252
277
  this.#buildViewBundle("user", this.#paths.userViews, this.#paths.userOutput),
253
278
  this.#buildViewBundle("framework", this.#paths.frameworkViews, this.#paths.frameworkOutput)
@@ -286,13 +311,14 @@ class Builder {
286
311
  this.#changedFiles.add(file);
287
312
  }
288
313
 
289
- const clientComponents = await this.#buildClientComponents(
314
+ const { clientFiles, rebuiltChunks } = await this.#buildClientComponents(
290
315
  userBundle,
291
316
  frameworkBundle,
292
317
  changedViews
293
318
  );
294
319
 
295
- this.#writeClientManifest(clientComponents);
320
+ this.#cache.lastRebuiltChunks = rebuiltChunks;
321
+ this.#writeClientManifest(clientFiles);
296
322
  this.#writeManifest();
297
323
  }
298
324
 
@@ -304,7 +330,7 @@ class Builder {
304
330
  ? path.join(this.#paths.templates, "vendor-dev.tsx")
305
331
  : path.join(this.#paths.templates, "vendor.tsx");
306
332
 
307
- await esbuild.build({
333
+ await this.#buildOnce("vendor", {
308
334
  entryPoints: [vendorFile],
309
335
  outfile,
310
336
  bundle: true,
@@ -316,6 +342,7 @@ class Builder {
316
342
  keepNames: true,
317
343
  jsx: "automatic"
318
344
  });
345
+
319
346
  this.#cache.vendorBuilt = true;
320
347
  }
321
348
 
@@ -325,7 +352,7 @@ class Builder {
325
352
  const outfile = path.join(this.#paths.jsOutput, "hmr.js");
326
353
  if (this.#cache.hmrBuilt && fs.existsSync(outfile)) return;
327
354
 
328
- await esbuild.build({
355
+ await this.#buildOnce("hmr", {
329
356
  entryPoints: [path.join(Paths.frameworkLib, "View/Client/hmr-client.js")],
330
357
  outfile,
331
358
  bundle: false,
@@ -334,14 +361,35 @@ class Builder {
334
361
  target: "es2020",
335
362
  minify: false
336
363
  });
364
+
337
365
  this.#cache.hmrBuilt = true;
338
366
  }
339
367
 
368
+ async #buildRefreshRuntime() {
369
+ if (!this.#isDev) return;
370
+
371
+ const outfile = path.join(this.#paths.jsOutput, "react-refresh-runtime.js");
372
+ if (this.#cache.refreshBuilt && fs.existsSync(outfile)) return;
373
+
374
+ await this.#buildOnce("refresh", {
375
+ entryPoints: [path.join(Paths.frameworkLib, "View/Client/refresh-entry.cjs")],
376
+ outfile,
377
+ bundle: true,
378
+ platform: "browser",
379
+ format: "iife",
380
+ globalName: "$RefreshRuntime$",
381
+ target: "es2020",
382
+ minify: false
383
+ });
384
+
385
+ this.#cache.refreshBuilt = true;
386
+ }
387
+
340
388
  async #buildSpaRuntime() {
341
389
  const outfile = path.join(this.#paths.jsOutput, "spa.js");
342
390
  if (this.#cache.spaBuilt && fs.existsSync(outfile)) return;
343
391
 
344
- await esbuild.build({
392
+ await this.#buildOnce("spa", {
345
393
  entryPoints: [path.join(Paths.frameworkLib, "View/Client/spa.js")],
346
394
  outfile,
347
395
  bundle: true,
@@ -350,6 +398,7 @@ class Builder {
350
398
  target: "es2020",
351
399
  minify: !this.#isDev
352
400
  });
401
+
353
402
  this.#cache.spaBuilt = true;
354
403
  }
355
404
 
@@ -359,7 +408,7 @@ class Builder {
359
408
 
360
409
  const consumerFile = path.join(this.#paths.templates, "rsc-consumer.tsx");
361
410
 
362
- await esbuild.build({
411
+ await this.#buildOnce("rsc", {
363
412
  entryPoints: [consumerFile],
364
413
  outfile,
365
414
  bundle: true,
@@ -375,9 +424,24 @@ class Builder {
375
424
  createCssStubPlugin()
376
425
  ]
377
426
  });
427
+
378
428
  this.#cache.rscConsumerBuilt = true;
379
429
  }
380
430
 
431
+ async #buildOnce(name, config) {
432
+ if (!this.#isDev) {
433
+ await esbuild.build(config);
434
+ return;
435
+ }
436
+
437
+ if (!this.#contexts.has(name)) {
438
+ const ctx = await esbuild.context(config);
439
+ this.#contexts.set(name, { ctx, entryKey: null });
440
+ }
441
+
442
+ await this.#contexts.get(name).ctx.rebuild();
443
+ }
444
+
381
445
  async #buildViewBundle(namespace, srcDir, outDir) {
382
446
  if (!fs.existsSync(srcDir)) {
383
447
  return { entries: [], layouts: [], meta: new Map(), importedBy: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
@@ -389,6 +453,16 @@ class Builder {
389
453
  return { entries: [], layouts: [], meta: new Map(), importedBy: new Map(), srcDir, namespace, changedFiles: [], changedSources: new Set() };
390
454
  }
391
455
 
456
+ if (this.#isDev) {
457
+ for (const [filePath, fileMeta] of meta.entries()) {
458
+ const resolvedImports = (fileMeta.imports || []).map(imp =>
459
+ this.#analyzer.resolveImport(filePath, imp)
460
+ );
461
+
462
+ this.#moduleGraph.addModule(filePath, resolvedImports, fileMeta.isClient || false);
463
+ }
464
+ }
465
+
392
466
  // Pre-scan: compute effective prop usage for server→client boundary filtering.
393
467
  // Must run before addToManifest so manifest gets effective (forward-aware) usage.
394
468
  const { effectiveMap, clientPathSet } = this.#preScanPropUsage(meta);
@@ -574,7 +648,9 @@ class Builder {
574
648
  this.#stats.islands = toBuild.length;
575
649
  }
576
650
 
577
- return clientFiles;
651
+ const rebuiltChunks = toBuild.map(c => c.chunkFile);
652
+
653
+ return { clientFiles, rebuiltChunks };
578
654
  }
579
655
 
580
656
  #writeClientManifest(clientComponents) {
@@ -629,11 +705,7 @@ class Builder {
629
705
  createPathAliasPlugin()
630
706
  ];
631
707
 
632
- // RSC plugin only for server builds — replaces "use client" with reference stubs.
633
- // Browser builds need the actual component code.
634
708
  if (isNode) {
635
- // Prop filter must be registered BEFORE client reference so its onLoad
636
- // handles "nitron-filtered" namespace before createClientReferencePlugin tries.
637
709
  if (options.effectiveMap && options.clientPaths) {
638
710
  plugins.push(createPropFilterPlugin(options.effectiveMap, options.clientPaths));
639
711
  }
@@ -650,6 +722,10 @@ class Builder {
650
722
  plugins.push(createServerFunctionsPlugin());
651
723
  }
652
724
 
725
+ if (this.#isDev && !isNode) {
726
+ plugins.push(createFastRefreshPlugin());
727
+ }
728
+
653
729
  const config = {
654
730
  entryPoints: entries,
655
731
  outdir: outDir,
@@ -678,7 +754,34 @@ class Builder {
678
754
  plugins.push(createOriginalJsxPlugin());
679
755
  }
680
756
 
681
- await esbuild.build(config);
757
+ if (!this.#isDev) {
758
+ await esbuild.build(config);
759
+ return;
760
+ }
761
+
762
+ const name = isNode ? "server" : "browser";
763
+ const entryKey = entries.slice().sort().join(",");
764
+ const cached = this.#contexts.get(name);
765
+
766
+ if (cached && cached.entryKey !== entryKey) {
767
+ await cached.ctx.dispose();
768
+ this.#contexts.delete(name);
769
+ }
770
+
771
+ if (!this.#contexts.has(name)) {
772
+ const ctx = await esbuild.context(config);
773
+ this.#contexts.set(name, { ctx, entryKey });
774
+ }
775
+
776
+ await this.#contexts.get(name).ctx.rebuild();
777
+ }
778
+
779
+ async dispose() {
780
+ for (const { ctx } of this.#contexts.values()) {
781
+ await ctx.dispose();
782
+ }
783
+
784
+ this.#contexts.clear();
682
785
  }
683
786
 
684
787
  #addToManifest(entries, layouts, meta, baseDir, namespace, effectiveMap) {
@@ -0,0 +1,84 @@
1
+ class ModuleGraph {
2
+ #nodes = new Map();
3
+
4
+ addModule(filePath, imports, isClient) {
5
+ const node = {
6
+ imports: new Set(imports),
7
+ importedBy: new Set(),
8
+ isHmrBoundary: isClient
9
+ };
10
+
11
+ this.#nodes.set(filePath, node);
12
+
13
+ for (const imp of imports) {
14
+ const impNode = this.#nodes.get(imp);
15
+
16
+ if (impNode) {
17
+ impNode.importedBy.add(filePath);
18
+ }
19
+ }
20
+ }
21
+
22
+ removeModule(filePath) {
23
+ const node = this.#nodes.get(filePath);
24
+ if (!node) return;
25
+
26
+ for (const imp of node.imports) {
27
+ const impNode = this.#nodes.get(imp);
28
+
29
+ if (impNode) {
30
+ impNode.importedBy.delete(filePath);
31
+ }
32
+ }
33
+
34
+ for (const parent of node.importedBy) {
35
+ const parentNode = this.#nodes.get(parent);
36
+
37
+ if (parentNode) {
38
+ parentNode.imports.delete(filePath);
39
+ }
40
+ }
41
+
42
+ this.#nodes.delete(filePath);
43
+ }
44
+
45
+ getAffectedBoundaries(changedFile) {
46
+ const boundaries = new Set();
47
+ const visited = new Set();
48
+
49
+ const walk = (file) => {
50
+ if (visited.has(file)) return;
51
+ visited.add(file);
52
+
53
+ const node = this.#nodes.get(file);
54
+ if (!node) return;
55
+
56
+ if (node.isHmrBoundary) {
57
+ boundaries.add(file);
58
+ return;
59
+ }
60
+
61
+ for (const parent of node.importedBy) {
62
+ walk(parent);
63
+ }
64
+ };
65
+
66
+ walk(changedFile);
67
+ return boundaries;
68
+ }
69
+
70
+ hasModule(filePath) {
71
+ return this.#nodes.has(filePath);
72
+ }
73
+
74
+ isClientComponent(filePath) {
75
+ const node = this.#nodes.get(filePath);
76
+ return node?.isHmrBoundary || false;
77
+ }
78
+
79
+ clear() {
80
+ this.#nodes.clear();
81
+ }
82
+ }
83
+
84
+ export default ModuleGraph;
@@ -91,8 +91,8 @@ class PropUsageAnalyzer {
91
91
  * @param {string} filePath - Absolute path to the .tsx/.jsx file.
92
92
  * @returns {{propUsage: object|null, forwarding: {forwards: Array, imports: object}|null, hasDefaultExport: boolean, translationKeys: string[]|null}}
93
93
  */
94
- static analyzeAll(filePath) {
95
- const parsed = parseFile(filePath);
94
+ static analyzeAll(filePath, cachedAst = null) {
95
+ const parsed = cachedAst ? { source: null, ast: cachedAst } : parseFile(filePath);
96
96
  if (!parsed) return { propUsage: null, forwarding: null, hasDefaultExport: false, translationKeys: null };
97
97
 
98
98
  // Extract translation keys BEFORE findComponentFunction (server files may not have components)
@@ -489,4 +489,39 @@ export function createClientReferencePlugin(isDev) {
489
489
  });
490
490
  }
491
491
  };
492
+ }
493
+
494
+ export function createFastRefreshPlugin() {
495
+ let transformSync = null;
496
+
497
+ return {
498
+ name: "fast-refresh",
499
+ setup(build) {
500
+ build.onLoad({ filter: /\.[jt]sx$/ }, async (args) => {
501
+ const source = await fs.promises.readFile(args.path, "utf8");
502
+
503
+ if (!hasUseClientDirective(source)) {
504
+ return null;
505
+ }
506
+
507
+ if (!transformSync) {
508
+ const oxc = await import("oxc-transform");
509
+ transformSync = oxc.transformSync;
510
+ }
511
+
512
+ const result = transformSync(args.path, source, {
513
+ jsx: { refresh: true }
514
+ });
515
+
516
+ if (result.errors?.length) {
517
+ return null;
518
+ }
519
+
520
+ return {
521
+ contents: result.code,
522
+ loader: args.path.endsWith(".tsx") ? "tsx" : "jsx"
523
+ };
524
+ });
525
+ }
526
+ };
492
527
  }