@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.
Files changed (59) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +2 -33
  7. package/lib/Build/Manager.js +390 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +177 -24
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +144 -1
  38. package/lib/Session/Redis.js +117 -0
  39. package/lib/Session/Session.js +0 -4
  40. package/lib/Support/Str.js +6 -4
  41. package/lib/Translation/Lang.js +376 -32
  42. package/lib/Translation/pluralize.js +81 -0
  43. package/lib/Validation/MagicBytes.js +120 -0
  44. package/lib/Validation/Validator.js +46 -29
  45. package/lib/View/Client/hmr-client.js +100 -90
  46. package/lib/View/Client/spa.js +121 -50
  47. package/lib/View/ClientManifest.js +60 -0
  48. package/lib/View/FlightRenderer.js +100 -0
  49. package/lib/View/Layout.js +0 -3
  50. package/lib/View/PropFilter.js +81 -0
  51. package/lib/View/View.js +230 -495
  52. package/lib/index.d.ts +22 -1
  53. package/package.json +3 -2
  54. package/skeleton/config/app.js +1 -0
  55. package/skeleton/config/server.js +13 -0
  56. package/skeleton/config/session.js +4 -0
  57. package/lib/Build/HydrationBuilder.js +0 -190
  58. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  59. package/lib/Console/Stubs/page-hydration.tsx +0 -53
package/lib/Auth/Auth.js CHANGED
@@ -134,7 +134,7 @@ class Auth {
134
134
  return res.redirect("/");
135
135
  }
136
136
 
137
- if (path.startsWith("/") || path.startsWith("http://") || path.startsWith("https://")) {
137
+ if (path.startsWith("/")) {
138
138
  return res.redirect(path);
139
139
  }
140
140
 
@@ -157,7 +157,7 @@ class Auth {
157
157
  return res.redirect("/");
158
158
  }
159
159
 
160
- if (path.startsWith("/") || path.startsWith("http://") || path.startsWith("https://")) {
160
+ if (path.startsWith("/")) {
161
161
  return res.redirect(path);
162
162
  }
163
163
 
@@ -10,14 +10,12 @@ import tailwindPostcss from "@tailwindcss/postcss";
10
10
  */
11
11
  class CssBuilder {
12
12
  #cache;
13
- #isDev;
14
13
  #cssInput;
15
14
  #cssOutput;
16
15
  #hasTailwind = null;
17
16
 
18
- constructor(cache, isDev, cssInput, cssOutput) {
17
+ constructor(cache, cssInput, cssOutput) {
19
18
  this.#cache = cache;
20
- this.#isDev = isDev;
21
19
  this.#cssInput = cssInput;
22
20
  this.#cssOutput = cssOutput;
23
21
  }
@@ -27,7 +25,7 @@ class CssBuilder {
27
25
  * @param {boolean} viewsChanged - Whether views have changed (forces Tailwind rebuild).
28
26
  * @returns {Promise<number>} Number of CSS files processed.
29
27
  */
30
- async build(viewsChanged) {
28
+ async build(viewsChanged, isDev = false) {
31
29
  if (!fs.existsSync(this.#cssInput)) {
32
30
  return 0;
33
31
  }
@@ -67,12 +65,12 @@ class CssBuilder {
67
65
  }
68
66
  const processor = hasTailwind ? this.#cache.tailwindProcessor : null;
69
67
 
70
- await Promise.all(filesToProcess.map(filename => this.#processCss(filename, hasTailwind, processor)));
68
+ await Promise.all(filesToProcess.map(filename => this.#processCss(filename, hasTailwind, processor, isDev)));
71
69
 
72
70
  return filesToProcess.length;
73
71
  }
74
72
 
75
- async #processCss(filename, hasTailwind, processor) {
73
+ async #processCss(filename, hasTailwind, processor, isDev) {
76
74
  const inputPath = path.join(this.#cssInput, filename);
77
75
  const outputPath = path.join(this.#cssOutput, filename);
78
76
 
@@ -92,7 +90,7 @@ class CssBuilder {
92
90
  const result = await processor.process(content, {
93
91
  from: inputPath,
94
92
  to: outputPath,
95
- map: this.#isDev ? { inline: false } : false
93
+ map: isDev ? { inline: false } : false
96
94
  });
97
95
 
98
96
  await fs.promises.writeFile(outputPath, result.css);
@@ -0,0 +1,174 @@
1
+ import path from "path";
2
+ import { mergeUsageTrees, resolveComponentPath } from "./propUtils.js";
3
+
4
+ /**
5
+ * Computes effective prop usage by merging child component prop usage
6
+ * back into parent components through JSX prop forwarding chains.
7
+ *
8
+ * Example: ParentComponent forwards data → ChildComponent.info
9
+ * ChildComponent uses: { title, description }
10
+ * ParentComponent effective data = { title, description }
11
+ * → unused fields are stripped at the server→client boundary
12
+ */
13
+ class EffectivePropUsage {
14
+ /**
15
+ * @param {Map<string, object|null>} propUsageMap - Basic prop usage per client component (from analyze()).
16
+ * @param {Map<string, {forwards: Array, imports: object}>} forwardMap - Forward info per component.
17
+ * @param {Set<string>} clientPaths - All client component absolute paths.
18
+ * @returns {Map<string, object|null>} Effective prop usage per component.
19
+ */
20
+ static compute(propUsageMap, forwardMap, clientPaths) {
21
+ const resolvedForwards = resolveForwards(forwardMap, clientPaths);
22
+ const { children, parents } = buildGraph(resolvedForwards);
23
+ const order = topologicalSort(clientPaths, children, parents);
24
+
25
+ const effectiveMap = new Map();
26
+
27
+ for (const [filePath, usage] of propUsageMap) {
28
+ effectiveMap.set(filePath, usage ? structuredClone(usage) : null);
29
+ }
30
+
31
+ for (const componentPath of order) {
32
+ const forwards = resolvedForwards.get(componentPath);
33
+ if (!forwards) continue;
34
+
35
+ let effective = effectiveMap.get(componentPath);
36
+ if (effective === null) continue;
37
+ if (!effective) effective = {};
38
+
39
+ for (const { parentProp, childPath, childProp } of forwards) {
40
+ const childEffective = effectiveMap.get(childPath);
41
+ let childPropUsage;
42
+
43
+ if (childEffective === null) {
44
+ childPropUsage = true;
45
+ }
46
+ else if (childEffective && childProp in childEffective) {
47
+ childPropUsage = childEffective[childProp];
48
+ }
49
+ else {
50
+ continue;
51
+ }
52
+
53
+ if (effective[parentProp] === true) {
54
+ // Basic usage was "whole" (likely from JSX forwarding itself).
55
+ // Replace with child's actual usage — this is the core optimization.
56
+ effective[parentProp] = childPropUsage;
57
+ }
58
+ else if (effective[parentProp] && typeof effective[parentProp] === "object") {
59
+ effective[parentProp] = mergeUsageTrees(effective[parentProp], childPropUsage);
60
+ }
61
+ else {
62
+ effective[parentProp] = childPropUsage;
63
+ }
64
+ }
65
+
66
+ effectiveMap.set(componentPath, effective);
67
+ }
68
+
69
+ return effectiveMap;
70
+ }
71
+ }
72
+
73
+
74
+ /**
75
+ * Resolves relative import paths in forwards to absolute file paths.
76
+ * Only keeps forwards to known client components.
77
+ */
78
+ function resolveForwards(forwardMap, clientPaths) {
79
+ const resolved = new Map();
80
+
81
+ for (const [filePath, info] of forwardMap) {
82
+ if (!info?.forwards?.length) continue;
83
+
84
+ const dir = path.dirname(filePath);
85
+ const items = [];
86
+
87
+ for (const fwd of info.forwards) {
88
+ const childPath = resolveComponentPath(fwd.childImport, dir);
89
+
90
+ if (childPath && clientPaths.has(childPath)) {
91
+ items.push({
92
+ parentProp: fwd.parentProp,
93
+ childPath,
94
+ childProp: fwd.childProp
95
+ });
96
+ }
97
+ }
98
+
99
+ if (items.length > 0) {
100
+ resolved.set(filePath, items);
101
+ }
102
+ }
103
+
104
+ return resolved;
105
+ }
106
+
107
+ /**
108
+ * Builds parent→children and child→parents adjacency lists from resolved forwards.
109
+ */
110
+ function buildGraph(resolvedForwards) {
111
+ const children = new Map();
112
+ const parents = new Map();
113
+
114
+ for (const [parentPath, forwards] of resolvedForwards) {
115
+ for (const fwd of forwards) {
116
+ if (!children.has(parentPath)) children.set(parentPath, new Set());
117
+ children.get(parentPath).add(fwd.childPath);
118
+
119
+ if (!parents.has(fwd.childPath)) parents.set(fwd.childPath, new Set());
120
+ parents.get(fwd.childPath).add(parentPath);
121
+ }
122
+ }
123
+
124
+ return { children, parents };
125
+ }
126
+
127
+ /**
128
+ * Topological sort — processes leaf components (no forwards) first,
129
+ * then parents, so child effective usage is ready when parent is processed.
130
+ *
131
+ * Uses Kahn's algorithm on the reversed forwarding graph.
132
+ */
133
+ function topologicalSort(allNodes, children, parents) {
134
+ const order = [];
135
+ const inDeg = new Map();
136
+
137
+ for (const node of allNodes) {
138
+ inDeg.set(node, children.get(node)?.size || 0);
139
+ }
140
+
141
+ const queue = [];
142
+
143
+ for (const [node, deg] of inDeg) {
144
+ if (deg === 0) queue.push(node);
145
+ }
146
+
147
+ const visited = new Set();
148
+ let qi = 0;
149
+
150
+ while (qi < queue.length) {
151
+ const node = queue[qi++];
152
+ order.push(node);
153
+ visited.add(node);
154
+
155
+ const parentSet = parents.get(node);
156
+ if (!parentSet) continue;
157
+
158
+ for (const parent of parentSet) {
159
+ const newDeg = inDeg.get(parent) - 1;
160
+ inDeg.set(parent, newDeg);
161
+
162
+ if (newDeg === 0) queue.push(parent);
163
+ }
164
+ }
165
+
166
+ // Nodes not visited = cycles. Add them at the end with basic usage.
167
+ for (const node of allNodes) {
168
+ if (!visited.has(node)) order.push(node);
169
+ }
170
+
171
+ return order;
172
+ }
173
+
174
+ export default EffectivePropUsage;
@@ -109,24 +109,4 @@ function transformFile(filePath) {
109
109
  return true;
110
110
  }
111
111
 
112
- /**
113
- * Transforms all built view files in a directory tree.
114
- */
115
- function transformDirectory(dir) {
116
- let count = 0;
117
-
118
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
119
- const full = path.join(dir, entry.name);
120
-
121
- if (entry.isDirectory()) {
122
- count += transformDirectory(full);
123
- }
124
- else if (entry.name.endsWith(".js") && !entry.name.endsWith(".map")) {
125
- if (transformFile(full)) count++;
126
- }
127
- }
128
-
129
- return count;
130
- }
131
-
132
- export { transformFile, transformDirectory };
112
+ export { transformFile };
@@ -63,7 +63,7 @@ class FileAnalyzer {
63
63
 
64
64
  this.#validateGraph(graph, entries, importedBy);
65
65
 
66
- return { entries, layouts, meta: graph };
66
+ return { entries, layouts, meta: graph, importedBy };
67
67
  }
68
68
 
69
69
  #findTsxFiles(dir, result = []) {
@@ -293,37 +293,6 @@ class FileAnalyzer {
293
293
  return resolved;
294
294
  }
295
295
 
296
- findClientComponents(file, meta, seen, depth = 0) {
297
- const result = new Set();
298
-
299
- if (depth > MAX_DEPTH || seen.has(file)) {
300
- return result;
301
- }
302
- seen.add(file);
303
-
304
- const fileMeta = meta.get(file);
305
- if (!fileMeta) {
306
- return result;
307
- }
308
-
309
- if (fileMeta.isClient) {
310
- result.add(file);
311
- }
312
-
313
- for (const importPath of fileMeta.imports) {
314
- const resolvedPath = this.resolveImport(file, importPath);
315
-
316
- if (meta.has(resolvedPath)) {
317
- const childComponents = this.findClientComponents(resolvedPath, meta, seen, depth + 1);
318
- for (const component of childComponents) {
319
- result.add(component);
320
- }
321
- }
322
- }
323
-
324
- return result;
325
- }
326
-
327
296
  collectCss(file, meta, seen, depth = 0) {
328
297
  if (depth > MAX_DEPTH || seen.has(file)) {
329
298
  return new Set();
@@ -366,7 +335,7 @@ class FileAnalyzer {
366
335
  throw this.#createError("Client Entry Imported", {
367
336
  Entry: relativePath(filePath),
368
337
  By: relativePath(importers[0]),
369
- Fix: "Use as island"
338
+ Fix: "Add \"use client\" directive"
370
339
  });
371
340
  }
372
341
  }