@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/index.d.ts CHANGED
@@ -259,10 +259,12 @@ export class Validator {
259
259
  }
260
260
 
261
261
  export class Lang {
262
- static get(key: string, replacements?: Record<string, any>): string;
262
+ static setup(server: any): void;
263
+ static get(key: string, params?: Record<string, any>): string;
263
264
  static has(key: string): boolean;
264
265
  static locale(): string;
265
266
  static setLocale(locale: string): void;
267
+ static getFilteredTranslations(locale: string, keys: string[] | null): Record<string, string>;
266
268
  }
267
269
 
268
270
  export class DateTime {
@@ -542,6 +544,24 @@ declare global {
542
544
  * const tab = request().query.tab || 'general';
543
545
  * const id = request().params.id;
544
546
  */
547
+ /**
548
+ * Translates a key using the current locale.
549
+ * Works on both SSR and client-side.
550
+ *
551
+ * @param key - Translation key (dot notation supported)
552
+ * @param params - Optional parameters for replacement (:name) and pluralization (count)
553
+ * @example
554
+ * __("welcome") // "Hoş geldiniz"
555
+ * __("messages.greeting", { name: "Burak" }) // "Merhaba Burak"
556
+ * __("messages.items", { count: 5 }) // "5 öğe"
557
+ */
558
+ function __(key: string, params?: Record<string, any>): string;
559
+
560
+ /**
561
+ * Alias for __() — translates a key using the current locale.
562
+ */
563
+ function lang(key: string, params?: Record<string, any>): string;
564
+
545
565
  function request(): {
546
566
  path: string;
547
567
  method: string;
@@ -551,6 +571,7 @@ declare global {
551
571
  cookies: Record<string, string>;
552
572
  ip: string;
553
573
  isAjax: boolean;
574
+ locale: string;
554
575
  session: {
555
576
  get<T = any>(key: string, defaultValue?: T): T;
556
577
  set(key: string, value: any): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitronjs/framework",
3
- "version": "0.2.26",
3
+ "version": "0.3.0",
4
4
  "description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
5
5
  "bin": {
6
6
  "njs": "./cli/njs.js"
@@ -35,7 +35,8 @@
35
35
  "postcss": "^8.5.6",
36
36
  "react": "^19.2.3",
37
37
  "react-dom": "^19.2.3",
38
- "react-refresh": "^0.18.0",
38
+ "react-server-dom-webpack": "^19.2.4",
39
+ "redis": "^5.6.0",
39
40
  "socket.io": "^4.8.1",
40
41
  "tailwindcss": "^4.1.18",
41
42
  "typescript": "^5.9.3"
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  locale: 'en',
3
+ fallback_locale: 'en',
3
4
  timezone: 'UTC',
4
5
 
5
6
  /**
@@ -11,6 +11,19 @@ export default {
11
11
  maxFiles: 10,
12
12
  maxParts: 60,
13
13
  attachFieldsToBody: true,
14
+ security: {
15
+ verifyMagicBytes: true,
16
+ dangerousExtensions: [
17
+ ".exe", ".bat", ".cmd", ".com", ".scr", ".pif", ".msi", ".dll",
18
+ ".sh", ".cgi", ".csh", ".ksh",
19
+ ".php", ".phtml", ".php5", ".php7",
20
+ ".jsp", ".asp", ".aspx",
21
+ ".vbs", ".vbe", ".wsf", ".wsh", ".ps1", ".psm1",
22
+ ".hta", ".cpl", ".inf", ".reg", ".sct"
23
+ ],
24
+ compoundExtensions: [".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst"],
25
+ allowDoubleExtension: false // WARNING: true enables file.exe.jpg to pass — security risk
26
+ },
14
27
  },
15
28
  },
16
29
 
@@ -1,6 +1,10 @@
1
1
  const SESSION_LIFETIME = 1000 * 60 * 60 * 2;
2
2
 
3
3
  export default {
4
+ // Driver: none | file | memory | redis
5
+ // WARNING: "memory" driver stores sessions in process memory.
6
+ // Sessions are lost on restart and memory usage grows with active users.
7
+ // Use "file" or "redis" in production.
4
8
  driver: "file",
5
9
  lifetime: SESSION_LIFETIME,
6
10
  cookieName: "session",
@@ -1,190 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import crypto from "crypto";
4
- import Paths from "../Core/Paths.js";
5
- import Layout from "../View/Layout.js";
6
-
7
- /**
8
- * Sanitizes a name to be a valid JavaScript identifier.
9
- * @param {string} name - The name to sanitize.
10
- * @returns {string} A valid JS identifier.
11
- */
12
- function sanitizeName(name) {
13
- return name.replace(/[^a-zA-Z0-9_$]/g, "_");
14
- }
15
-
16
- /**
17
- * Generates hydration scripts for client-side component rendering.
18
- * Creates island bundles for React components that need client interactivity.
19
- */
20
- class HydrationBuilder {
21
- #cache;
22
- #isDev;
23
- #templatesPath;
24
- #analyzer;
25
-
26
- constructor(cache, isDev, templatesPath, analyzer) {
27
- this.#cache = cache;
28
- this.#isDev = isDev;
29
- this.#templatesPath = templatesPath;
30
- this.#analyzer = analyzer;
31
- }
32
-
33
- /**
34
- * Builds hydration bundles for client components.
35
- * @param {object} userBundle - User view bundle.
36
- * @param {object} frameworkBundle - Framework view bundle.
37
- * @param {object} manifest - Build manifest.
38
- * @param {Set<string>|null} changedViews - Changed view files or null for full build.
39
- * @returns {Promise<Array<{input: string, output: string}>>}
40
- */
41
- async build(userBundle, frameworkBundle, manifest, changedViews = null) {
42
- const hydrationFiles = [];
43
- const allChangedFiles = new Set();
44
-
45
- if (changedViews) {
46
- for (const v of changedViews) allChangedFiles.add(v);
47
- }
48
-
49
- for (const bundle of [userBundle, frameworkBundle]) {
50
- if (!bundle?.meta) continue;
51
- for (const [filePath, fileMeta] of bundle.meta.entries()) {
52
- if (fileMeta.isClient || fileMeta.needsClient) {
53
- const content = fs.readFileSync(filePath, "utf8");
54
- const hash = crypto.createHash("md5").update(content).digest("hex");
55
- const cacheKey = `hydration:${filePath}`;
56
- const cachedHash = this.#cache.fileHashes.get(cacheKey);
57
-
58
- if (cachedHash !== hash) {
59
- this.#cache.fileHashes.set(cacheKey, hash);
60
- allChangedFiles.add(filePath);
61
- }
62
- }
63
- }
64
- }
65
-
66
- for (const bundle of [userBundle, frameworkBundle]) {
67
- if (!bundle?.entries.length) continue;
68
-
69
- for (const viewPath of bundle.entries) {
70
- const viewRelative = path.relative(bundle.srcDir, viewPath)
71
- .replace(/\.tsx$/, "")
72
- .replace(/\\/g, "/")
73
- .toLowerCase();
74
- const manifestKey = `${bundle.namespace}:${viewRelative}`;
75
- const hydrationScriptPath = `/js/${viewRelative}.js`;
76
- const hydrationOutputPath = path.join(Paths.publicJs, viewRelative + ".js");
77
-
78
- const viewMeta = bundle.meta.get(viewPath);
79
- const clientComponents = new Set();
80
-
81
- if (!viewMeta?.layoutDisabled) {
82
- const viewRelativeForLayout = path.relative(bundle.srcDir, viewPath).replace(/\\/g, "/");
83
- const layoutChain = Layout.resolve(viewRelativeForLayout, bundle.srcDir);
84
-
85
- for (const layout of layoutChain) {
86
- const layoutClients = this.#analyzer.findClientComponents(layout.path, bundle.meta, new Set());
87
- for (const c of layoutClients) {
88
- clientComponents.add(c);
89
- }
90
- }
91
- }
92
-
93
- const viewClients = this.#analyzer.findClientComponents(viewPath, bundle.meta, new Set());
94
- for (const c of viewClients) {
95
- clientComponents.add(c);
96
- }
97
-
98
- const needsRebuild = !changedViews ||
99
- allChangedFiles.has(viewPath) ||
100
- [...clientComponents].some(c => allChangedFiles.has(c));
101
-
102
- if (clientComponents.size) {
103
- if (needsRebuild || !fs.existsSync(hydrationOutputPath)) {
104
- const hydrationFile = this.#generateHydrationFile(
105
- viewPath,
106
- bundle.srcDir,
107
- clientComponents,
108
- bundle.meta
109
- );
110
- hydrationFiles.push(hydrationFile);
111
- }
112
- manifest[manifestKey].hydrationScript = hydrationScriptPath;
113
- }
114
- }
115
- }
116
-
117
- return hydrationFiles;
118
- }
119
-
120
- #generateHydrationFile(viewPath, srcDir, clientComponents, meta) {
121
- const templateName = this.#isDev ? "page-hydration-dev.tsx" : "page-hydration.tsx";
122
- const cacheKey = this.#isDev ? "hydrationTemplateDev" : "hydrationTemplate";
123
-
124
- if (!this.#cache[cacheKey]) {
125
- const templatePath = path.join(this.#templatesPath, templateName);
126
- this.#cache[cacheKey] = fs.readFileSync(templatePath, "utf8");
127
- }
128
-
129
- const viewRelative = path.relative(srcDir, viewPath).replace(/\.tsx$/, "").toLowerCase();
130
- const outputDir = path.join(Paths.project, ".nitron/hydration", path.dirname(viewRelative));
131
- const outputFile = path.join(outputDir, path.basename(viewRelative) + ".tsx");
132
- const moduleId = viewRelative.replace(/[\/\\]/g, "_");
133
-
134
- fs.mkdirSync(outputDir, { recursive: true });
135
-
136
- const imports = [];
137
- const manifestEntries = [];
138
- const registrations = [];
139
- let index = 0;
140
-
141
- for (const componentPath of clientComponents) {
142
- const componentMeta = meta.get(componentPath);
143
- if (!componentMeta) continue;
144
-
145
- const baseName = path.basename(componentPath, ".tsx");
146
- const relativePath = path.relative(outputDir, componentPath)
147
- .replace(/\\/g, "/")
148
- .replace(/\.tsx$/, "");
149
-
150
- if (componentMeta.hasDefault) {
151
- const importName = sanitizeName(baseName) + "_" + index++;
152
- imports.push(`import ${importName} from "${relativePath}";`);
153
- manifestEntries.push(` "${baseName}": ${importName}`);
154
- manifestEntries.push(` [${importName}.displayName || ${importName}.name || "${baseName}"]: ${importName}`);
155
- if (this.#isDev) {
156
- registrations.push(`if (window.__NITRON_REFRESH__) window.__NITRON_REFRESH__.register(${importName}, "${moduleId}_${importName}");`);
157
- }
158
- }
159
-
160
- for (const namedExport of componentMeta.named || []) {
161
- const importName = sanitizeName(namedExport) + "_" + index++;
162
- imports.push(`import { ${namedExport} as ${importName} } from "${relativePath}";`);
163
- manifestEntries.push(` "${namedExport}": ${importName}`);
164
- manifestEntries.push(` [${importName}.displayName || ${importName}.name || "${namedExport}"]: ${importName}`);
165
- if (this.#isDev) {
166
- registrations.push(`if (window.__NITRON_REFRESH__) window.__NITRON_REFRESH__.register(${importName}, "${moduleId}_${importName}");`);
167
- }
168
- }
169
- }
170
-
171
- let code = this.#cache[cacheKey]
172
- .replace("// __COMPONENT_IMPORTS__", imports.join("\n"))
173
- .replace(
174
- "// __COMPONENT_MANIFEST__",
175
- `Object.assign(componentManifest, {\n${manifestEntries.join(",\n")}\n});`
176
- );
177
-
178
- if (this.#isDev) {
179
- code = code
180
- .replace("__NITRON_MODULE_ID__", moduleId)
181
- .replace("// __COMPONENT_REGISTRATIONS__", registrations.join("\n"));
182
- }
183
-
184
- fs.writeFileSync(outputFile, code);
185
-
186
- return outputFile;
187
- }
188
- }
189
-
190
- export default HydrationBuilder;
@@ -1,72 +0,0 @@
1
- import React from "react";
2
- import { createRoot } from "react-dom/client";
3
-
4
- // __COMPONENT_IMPORTS__
5
-
6
- const componentManifest: Record<string, React.ComponentType<any>> = {};
7
-
8
- // __COMPONENT_MANIFEST__
9
-
10
- const w = window as any;
11
- const prevRefreshReg = w.$RefreshReg$;
12
- const prevRefreshSig = w.$RefreshSig$;
13
-
14
- if (typeof w.$RefreshReg$ === "function") {
15
- const moduleId = "__NITRON_MODULE_ID__";
16
- w.$RefreshReg$ = (type: any, id: string) => {
17
- const fullId = moduleId + "_" + id;
18
- if (prevRefreshReg) prevRefreshReg(type, fullId);
19
- };
20
- }
21
-
22
- if (typeof w.$RefreshSig$ === "function") {
23
- w.$RefreshSig$ = prevRefreshSig;
24
- }
25
-
26
- // __COMPONENT_REGISTRATIONS__
27
-
28
- function mount() {
29
- const props = (window as any).__NITRON_PROPS__ || {};
30
- if (!(window as any).__NITRON_ROOTS__) (window as any).__NITRON_ROOTS__ = new Map();
31
-
32
- const islands = document.querySelectorAll<HTMLElement>("[data-cid]");
33
-
34
- islands.forEach(element => {
35
- const componentName = element.dataset.island;
36
- const componentId = element.dataset.cid;
37
-
38
- if (!componentName || !componentId) return;
39
-
40
- const Component = componentManifest[componentName];
41
- if (!Component) return;
42
-
43
- const componentProps = props[componentId] || {};
44
-
45
- try {
46
- const roots = (window as any).__NITRON_ROOTS__ as Map<HTMLElement, any>;
47
- let root = roots.get(element);
48
- if (!root) {
49
- root = createRoot(element);
50
- roots.set(element, root);
51
- }
52
- root.render(React.createElement(Component, componentProps));
53
- } catch {
54
- }
55
- });
56
-
57
- delete (window as any).__NITRON_PROPS__;
58
-
59
- if ((window as any).__NITRON_REFRESH__) {
60
- (window as any).__NITRON_REFRESH__.performReactRefresh();
61
- }
62
- }
63
-
64
- const isHmrUpdate = (window as any).__NITRON_HMR_UPDATE__;
65
-
66
- if (!isHmrUpdate) {
67
- if (document.readyState === "loading") {
68
- document.addEventListener("DOMContentLoaded", mount);
69
- } else {
70
- mount();
71
- }
72
- }
@@ -1,53 +0,0 @@
1
- import React from "react";
2
- import { createRoot } from "react-dom/client";
3
-
4
- // __COMPONENT_IMPORTS__
5
-
6
- declare global {
7
- interface Window {
8
- __NITRON_PROPS__?: Record<string, any>;
9
- __NITRON_ROOTS__?: Map<HTMLElement, any>;
10
- }
11
- }
12
-
13
- const componentManifest: Record<string, React.ComponentType<any>> = {};
14
-
15
- // __COMPONENT_MANIFEST__
16
-
17
- function mount() {
18
- const props = window.__NITRON_PROPS__ || {};
19
- if (!window.__NITRON_ROOTS__) window.__NITRON_ROOTS__ = new Map();
20
-
21
- const islands = document.querySelectorAll<HTMLElement>("[data-cid]");
22
-
23
- islands.forEach(element => {
24
- const componentName = element.dataset.island;
25
- const componentId = element.dataset.cid;
26
-
27
- if (!componentName || !componentId) return;
28
-
29
- const Component = componentManifest[componentName];
30
- if (!Component) return;
31
-
32
- const componentProps = props[componentId] || {};
33
-
34
- try {
35
- let root = window.__NITRON_ROOTS__.get(element);
36
- if (!root) {
37
- root = createRoot(element);
38
- window.__NITRON_ROOTS__.set(element, root);
39
- }
40
- root.render(React.createElement(Component, componentProps));
41
- } catch {
42
- }
43
- });
44
-
45
- delete window.__NITRON_PROPS__;
46
- }
47
-
48
- if (document.readyState === "loading") {
49
- document.addEventListener("DOMContentLoaded", mount);
50
- } else {
51
- mount();
52
- }
53
-