@nitronjs/framework 0.1.24 → 0.2.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 (37) hide show
  1. package/lib/Build/CssBuilder.js +129 -0
  2. package/lib/Build/FileAnalyzer.js +395 -0
  3. package/lib/Build/HydrationBuilder.js +173 -0
  4. package/lib/Build/Manager.js +290 -943
  5. package/lib/Build/colors.js +10 -0
  6. package/lib/Build/jsxRuntime.js +116 -0
  7. package/lib/Build/plugins.js +264 -0
  8. package/lib/Console/Commands/BuildCommand.js +6 -5
  9. package/lib/Console/Commands/DevCommand.js +151 -311
  10. package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
  11. package/lib/Console/Stubs/page-hydration.tsx +9 -10
  12. package/lib/Console/Stubs/vendor-dev.tsx +50 -0
  13. package/lib/Core/Environment.js +29 -2
  14. package/lib/Core/Paths.js +12 -4
  15. package/lib/Database/Drivers/MySQLDriver.js +5 -4
  16. package/lib/Database/QueryBuilder.js +2 -3
  17. package/lib/Filesystem/Manager.js +32 -7
  18. package/lib/HMR/Server.js +87 -0
  19. package/lib/Http/Server.js +9 -5
  20. package/lib/Logging/Manager.js +68 -18
  21. package/lib/Route/Loader.js +3 -4
  22. package/lib/Route/Manager.js +24 -3
  23. package/lib/Runtime/Entry.js +26 -1
  24. package/lib/Session/File.js +18 -7
  25. package/lib/View/Client/hmr-client.js +166 -0
  26. package/lib/View/Client/spa.js +142 -0
  27. package/lib/View/Layout.js +94 -0
  28. package/lib/View/Manager.js +390 -46
  29. package/lib/index.d.ts +55 -0
  30. package/package.json +2 -1
  31. package/skeleton/.env.example +0 -2
  32. package/skeleton/app/Controllers/HomeController.js +27 -3
  33. package/skeleton/config/app.js +15 -14
  34. package/skeleton/config/session.js +1 -1
  35. package/skeleton/globals.d.ts +3 -63
  36. package/skeleton/resources/views/Site/Home.tsx +274 -50
  37. package/skeleton/tsconfig.json +5 -1
@@ -0,0 +1,10 @@
1
+ const COLORS = {
2
+ reset: "\x1b[0m",
3
+ dim: "\x1b[2m",
4
+ red: "\x1b[31m",
5
+ green: "\x1b[32m",
6
+ yellow: "\x1b[33m",
7
+ cyan: "\x1b[36m"
8
+ };
9
+
10
+ export default COLORS;
@@ -0,0 +1,116 @@
1
+ const JSX_RUNTIME = `
2
+ import * as React from 'react';
3
+ import * as OriginalJsx from '__react_jsx_original__';
4
+
5
+ const CTX = Symbol.for('__nitron_view_context__');
6
+ const MARK = Symbol.for('__nitron_client_component__');
7
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
8
+
9
+ function getContext() {
10
+ return globalThis[CTX]?.getStore?.();
11
+ }
12
+
13
+ globalThis.csrf = () => getContext()?.csrf || '';
14
+
15
+ globalThis.request = () => {
16
+ const ctx = getContext();
17
+ return ctx?.request || { params: {}, query: {}, url: '', method: 'GET', headers: {} };
18
+ };
19
+
20
+ const DepthContext = React.createContext(false);
21
+ const componentCache = new WeakMap();
22
+
23
+ function sanitizeProps(obj, seen = new WeakSet()) {
24
+ if (obj == null) return obj;
25
+
26
+ const type = typeof obj;
27
+ if (type === 'function' || type === 'symbol') return undefined;
28
+ if (type === 'bigint') return obj.toString();
29
+ if (type !== 'object') return obj;
30
+
31
+ if (seen.has(obj)) return undefined;
32
+ seen.add(obj);
33
+
34
+ if (Array.isArray(obj)) {
35
+ return obj.map(item => {
36
+ const sanitized = sanitizeProps(item, seen);
37
+ return sanitized === undefined ? null : sanitized;
38
+ });
39
+ }
40
+
41
+ if (obj instanceof Date) return obj.toISOString();
42
+ if (obj._attributes && typeof obj._attributes === 'object') {
43
+ return sanitizeProps(obj._attributes, seen);
44
+ }
45
+ if (typeof obj.toJSON === 'function') {
46
+ return sanitizeProps(obj.toJSON(), seen);
47
+ }
48
+
49
+ const proto = Object.getPrototypeOf(obj);
50
+ if (proto !== Object.prototype && proto !== null) return undefined;
51
+
52
+ const result = {};
53
+ for (const key of Object.keys(obj)) {
54
+ if (UNSAFE_KEYS.has(key)) continue;
55
+ const value = sanitizeProps(obj[key], seen);
56
+ if (value !== undefined) result[key] = value;
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function wrapWithDepth(children) {
62
+ return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
63
+ }
64
+
65
+ function createIsland(Component, name) {
66
+ function IslandBoundary(props) {
67
+ if (React.useContext(DepthContext)) {
68
+ return OriginalJsx.jsx(Component, props);
69
+ }
70
+
71
+ const id = React.useId();
72
+ const safeProps = sanitizeProps(props) || {};
73
+
74
+ const ctx = getContext();
75
+ if (ctx) {
76
+ ctx.props = ctx.props || {};
77
+ ctx.props[id] = safeProps;
78
+ }
79
+
80
+ return OriginalJsx.jsx('div', {
81
+ 'data-cid': id,
82
+ 'data-island': name,
83
+ children: wrapWithDepth(OriginalJsx.jsx(Component, props))
84
+ });
85
+ }
86
+
87
+ IslandBoundary.displayName = 'Island(' + name + ')';
88
+ return IslandBoundary;
89
+ }
90
+
91
+ function getWrappedComponent(Component) {
92
+ if (!componentCache.has(Component)) {
93
+ const name = Component.displayName || Component.name || 'Anonymous';
94
+ componentCache.set(Component, createIsland(Component, name));
95
+ }
96
+ return componentCache.get(Component);
97
+ }
98
+
99
+ export function jsx(type, props, key) {
100
+ if (typeof type === 'function' && type[MARK]) {
101
+ return OriginalJsx.jsx(getWrappedComponent(type), props, key);
102
+ }
103
+ return OriginalJsx.jsx(type, props, key);
104
+ }
105
+
106
+ export function jsxs(type, props, key) {
107
+ if (typeof type === 'function' && type[MARK]) {
108
+ return OriginalJsx.jsx(getWrappedComponent(type), props, key);
109
+ }
110
+ return OriginalJsx.jsxs(type, props, key);
111
+ }
112
+
113
+ export const Fragment = OriginalJsx.Fragment;
114
+ `;
115
+
116
+ export default JSX_RUNTIME;
@@ -0,0 +1,264 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parse } from "@babel/parser";
4
+ import traverse from "@babel/traverse";
5
+ import Paths from "../Core/Paths.js";
6
+ import COLORS from "./colors.js";
7
+
8
+ const _traverse = traverse.default;
9
+
10
+ export function createPathAliasPlugin() {
11
+ const root = Paths.project;
12
+ return {
13
+ name: "path-alias",
14
+ setup: (build) => {
15
+ // @ resolves to project root
16
+ build.onResolve({ filter: /^@\// }, async (args) => {
17
+ const relativePath = args.path.replace(/^@\//, "");
18
+ const absolutePath = path.join(root, relativePath);
19
+
20
+ const extensions = [".js", ".ts", ".jsx", ".tsx", ""];
21
+ for (const ext of extensions) {
22
+ const fullPath = absolutePath + ext;
23
+ if (fs.existsSync(fullPath)) {
24
+ return { path: fullPath, external: false };
25
+ }
26
+ }
27
+
28
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
29
+ for (const ext of [".js", ".ts", ".jsx", ".tsx"]) {
30
+ const indexPath = path.join(absolutePath, "index" + ext);
31
+ if (fs.existsSync(indexPath)) {
32
+ return { path: indexPath, external: false };
33
+ }
34
+ }
35
+ }
36
+
37
+ return { path: absolutePath + ".js", external: false };
38
+ });
39
+ }
40
+ };
41
+ }
42
+
43
+ export function createOriginalJsxPlugin() {
44
+ return {
45
+ name: "original-jsx",
46
+ setup: (build) => {
47
+ build.onResolve({ filter: /^__react_jsx_original__$/ }, () => ({
48
+ path: "react/jsx-runtime",
49
+ external: true
50
+ }));
51
+ }
52
+ };
53
+ }
54
+
55
+ export function createVendorGlobalsPlugin() {
56
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
57
+
58
+ const packages = {
59
+ "react": "__NITRON_REACT__",
60
+ "react-dom": "__NITRON_REACT_DOM__",
61
+ "react-dom/client": "__NITRON_REACT_DOM_CLIENT__",
62
+ "react/jsx-runtime": "__NITRON_JSX_RUNTIME__"
63
+ };
64
+
65
+ const patterns = Object.entries(packages).map(([pkg, global]) => ({
66
+ filter: new RegExp(`^${escapeRegex(pkg)}$`),
67
+ pkg,
68
+ global
69
+ }));
70
+
71
+ return {
72
+ name: "vendor-globals",
73
+ setup: (build) => {
74
+ for (const { filter, pkg, global } of patterns) {
75
+ build.onResolve({ filter }, () => ({
76
+ path: pkg,
77
+ namespace: "vendor-global"
78
+ }));
79
+
80
+ build.onLoad({ filter, namespace: "vendor-global" }, () => ({
81
+ contents: `module.exports = window.${global};`,
82
+ loader: "js"
83
+ }));
84
+ }
85
+ }
86
+ };
87
+ }
88
+
89
+ export function createServerModuleBlockerPlugin() {
90
+ const SERVER_MODULES = /lib\/(DB|Mail|Log|Hash|Environment|Server|Model|Validator|Storage)\.js$/;
91
+
92
+ return {
93
+ name: "server-module-blocker",
94
+ setup: (build) => {
95
+ build.onResolve({ filter: SERVER_MODULES }, (args) => {
96
+ const moduleName = args.path.match(/\/(\w+)\.js$/)?.[1] || "Module";
97
+ return {
98
+ path: args.path,
99
+ namespace: "server-blocked",
100
+ pluginData: { moduleName, importer: args.importer }
101
+ };
102
+ });
103
+
104
+ build.onLoad({ filter: /.*/, namespace: "server-blocked" }, (args) => {
105
+ const { moduleName, importer } = args.pluginData;
106
+ const importerRelative = importer ? path.relative(Paths.project, importer) : "unknown";
107
+
108
+ return {
109
+ errors: [{
110
+ text: `Server module "${moduleName}" cannot be imported in client components`,
111
+ notes: [{
112
+ text: `Imported from: ${importerRelative}\n\nClient components run in the browser and cannot access server-only modules.\nMove this logic to a server component or use an API endpoint.`
113
+ }]
114
+ }]
115
+ };
116
+ });
117
+ }
118
+ };
119
+ }
120
+
121
+ export function createServerFunctionsPlugin() {
122
+ return {
123
+ name: "server-functions",
124
+ setup: (build) => {
125
+ build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
126
+ if (args.path.includes("node_modules")) {
127
+ return null;
128
+ }
129
+
130
+ let source = await fs.promises.readFile(args.path, "utf8");
131
+
132
+ if (!source.includes("csrf(") && !source.includes("route(")) {
133
+ return null;
134
+ }
135
+
136
+ source = source.replace(
137
+ /\bcsrf\s*\(\s*\)/g,
138
+ "window.__NITRON_RUNTIME__.csrf"
139
+ );
140
+
141
+ source = source.replace(
142
+ /\broute\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
143
+ (_, routeName) => `window.__NITRON_RUNTIME__.routes["${routeName}"]`
144
+ );
145
+
146
+ const ext = args.path.split(".").pop();
147
+ const loader = ext === "tsx" ? "tsx" : ext === "ts" ? "ts" : ext === "jsx" ? "jsx" : "js";
148
+
149
+ return { contents: source, loader };
150
+ });
151
+ }
152
+ };
153
+ }
154
+
155
+ export function createCssStubPlugin() {
156
+ return {
157
+ name: "css-stub",
158
+ setup: (build) => {
159
+ build.onResolve({ filter: /\.css$/ }, (args) => ({
160
+ path: args.path,
161
+ namespace: "css-stub"
162
+ }));
163
+
164
+ build.onLoad({ filter: /.*/, namespace: "css-stub" }, () => ({
165
+ contents: "",
166
+ loader: "js"
167
+ }));
168
+ }
169
+ };
170
+ }
171
+
172
+ export function createMarkerPlugin(options, isDev) {
173
+ return {
174
+ name: "client-marker",
175
+ setup: (build) => {
176
+ if (options.platform === "browser") {
177
+ return;
178
+ }
179
+
180
+ build.onLoad({ filter: /\.tsx$/ }, async (args) => {
181
+ const source = await fs.promises.readFile(args.path, "utf8");
182
+
183
+ if (!/^\s*["']use client["']/.test(source.slice(0, 50))) {
184
+ return null;
185
+ }
186
+
187
+ let ast;
188
+ try {
189
+ ast = parse(source, {
190
+ sourceType: "module",
191
+ plugins: ["typescript", "jsx"]
192
+ });
193
+ } catch {
194
+ if (isDev) {
195
+ console.warn(`${COLORS.yellow}⚠ Parse: ${args.path}${COLORS.reset}`);
196
+ }
197
+ return null;
198
+ }
199
+
200
+ const exports = findExports(ast);
201
+ if (!exports.length) {
202
+ return null;
203
+ }
204
+
205
+ const symbolCode = `Symbol.for('__nitron_client_component__')`;
206
+ let additionalCode = "\n";
207
+
208
+ for (const exp of exports) {
209
+ additionalCode += `try { Object.defineProperty(${exp.name}, ${symbolCode}, { value: true }); ${exp.name}.displayName = "${exp.name}"; } catch {}\n`;
210
+ }
211
+
212
+ return { contents: source + additionalCode, loader: "tsx" };
213
+ });
214
+ }
215
+ };
216
+ }
217
+
218
+ function findExports(ast) {
219
+ const exports = [];
220
+
221
+ _traverse(ast, {
222
+ ExportDefaultDeclaration: (p) => {
223
+ const decl = p.node.declaration;
224
+ let name = "__default__";
225
+
226
+ if (decl.type === "Identifier") {
227
+ name = decl.name;
228
+ } else if (decl.type === "FunctionDeclaration" && decl.id?.name) {
229
+ name = decl.id.name;
230
+ } else if (decl.type === "CallExpression" &&
231
+ ["memo", "forwardRef", "lazy"].includes(decl.callee?.name)) {
232
+ name = decl.arguments[0]?.name || "__default__";
233
+ }
234
+
235
+ exports.push({ name });
236
+ },
237
+
238
+ ExportNamedDeclaration: (p) => {
239
+ for (const spec of p.node.specifiers || []) {
240
+ if (spec.type === "ExportSpecifier") {
241
+ exports.push({
242
+ name: spec.exported.name === "default" ? spec.local.name : spec.exported.name
243
+ });
244
+ }
245
+ }
246
+
247
+ const decl = p.node.declaration;
248
+ if (decl?.type === "FunctionDeclaration" && decl.id?.name) {
249
+ exports.push({ name: decl.id.name });
250
+ }
251
+ if (decl?.type === "VariableDeclaration") {
252
+ for (const d of decl.declarations) {
253
+ if (d.id.type === "Identifier" && d.init?.type === "ArrowFunctionExpression") {
254
+ exports.push({ name: d.id.name });
255
+ }
256
+ }
257
+ }
258
+ }
259
+ });
260
+
261
+ return exports;
262
+ }
263
+
264
+ export { findExports };
@@ -1,25 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import Builder from "../../Build/Manager.js";
3
3
 
4
- export default async function Build(only = null) {
4
+ export default async function Build(only = null, isDev = false) {
5
5
  const builder = new Builder();
6
6
 
7
7
  try {
8
- const success = await builder.run(only);
8
+ const success = await builder.run(only, isDev);
9
9
  return success;
10
- } catch (error) {
10
+ }
11
+ catch (error) {
11
12
  console.error("Build failed:", error.message);
12
13
  throw error;
13
14
  }
14
15
  }
15
16
 
16
- // Auto-run when called directly
17
+ // Auto-run when called directly (production build)
17
18
  const isMain = process.argv[1]?.endsWith("BuildCommand.js");
18
19
  if (isMain) {
19
20
  // process.argv[2] would be --only=views or --only=css
20
21
  const onlyArg = process.argv.find(arg => arg.startsWith("--only="));
21
22
  const only = onlyArg?.split("=")[1] || null;
22
- Build(only)
23
+ Build(only, false) // Production build
23
24
  .then(success => process.exit(success ? 0 : 1))
24
25
  .catch(() => process.exit(1));
25
26
  }