@nitronjs/framework 0.3.2 → 0.3.4
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/cli/njs.js +1 -1
- package/lib/Build/CssBuilder.js +152 -136
- package/lib/Build/FileAnalyzer.js +52 -35
- package/lib/Build/Manager.js +144 -41
- package/lib/Build/ModuleGraph.js +84 -0
- package/lib/Build/PropUsageAnalyzer.js +2 -2
- package/lib/Build/plugins.js +35 -0
- package/lib/Console/Commands/DevCommand.js +301 -258
- package/lib/Date/DateTime.js +22 -2
- package/lib/Dev/DevIndicator.js +2 -1
- package/lib/HMR/Server.js +93 -170
- package/lib/Runtime/Entry.js +3 -0
- package/lib/View/Client/hmr-client.js +217 -219
- package/lib/View/Client/refresh-entry.cjs +1 -0
- package/lib/View/View.js +12 -7
- package/lib/index.d.ts +2 -0
- package/package.json +8 -6
- /package/skeleton/database/migrations/{2025_01_01_00_00_users.js → 2025_01_01_00_00_create_users.js} +0 -0
package/cli/njs.js
CHANGED
package/lib/Build/CssBuilder.js
CHANGED
|
@@ -1,136 +1,152 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import crypto from "crypto";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.#
|
|
19
|
-
this.#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (!
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
|
|
5
|
+
class CssBuilder {
|
|
6
|
+
#cache;
|
|
7
|
+
#cssInput;
|
|
8
|
+
#cssOutput;
|
|
9
|
+
#hasTailwind = null;
|
|
10
|
+
#compiler = null;
|
|
11
|
+
#lastCompilerHash = null;
|
|
12
|
+
#scanner = null;
|
|
13
|
+
#projectBase;
|
|
14
|
+
|
|
15
|
+
constructor(cache, cssInput, cssOutput) {
|
|
16
|
+
this.#cache = cache;
|
|
17
|
+
this.#cssInput = cssInput;
|
|
18
|
+
this.#cssOutput = cssOutput;
|
|
19
|
+
this.#projectBase = path.resolve(cssInput, "../..");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async build(viewsChanged, isDev = false) {
|
|
23
|
+
if (!fs.existsSync(this.#cssInput)) {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cssFiles = fs.readdirSync(this.#cssInput).filter(f => f.endsWith(".css"));
|
|
28
|
+
|
|
29
|
+
if (!cssFiles.length) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasTailwind = this.#detectTailwind();
|
|
34
|
+
const filesToProcess = [];
|
|
35
|
+
|
|
36
|
+
for (const filename of cssFiles) {
|
|
37
|
+
const filePath = path.join(this.#cssInput, filename);
|
|
38
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
39
|
+
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
40
|
+
const cachedHash = this.#cache.cssHashes.get(filePath);
|
|
41
|
+
const outputPath = path.join(this.#cssOutput, filename);
|
|
42
|
+
const outputExists = fs.existsSync(outputPath);
|
|
43
|
+
|
|
44
|
+
if (cachedHash !== hash || !outputExists) {
|
|
45
|
+
this.#cache.css.set(filePath, content);
|
|
46
|
+
this.#cache.cssHashes.set(filePath, hash);
|
|
47
|
+
filesToProcess.push(filename);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (hasTailwind && viewsChanged && !filesToProcess.length) {
|
|
52
|
+
filesToProcess.push(...cssFiles);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!filesToProcess.length) return 0;
|
|
56
|
+
|
|
57
|
+
await Promise.all(filesToProcess.map(filename => this.#processCss(filename, hasTailwind, isDev)));
|
|
58
|
+
|
|
59
|
+
return filesToProcess.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #processCss(filename, hasTailwind, isDev) {
|
|
63
|
+
const inputPath = path.join(this.#cssInput, filename);
|
|
64
|
+
const outputPath = path.join(this.#cssOutput, filename);
|
|
65
|
+
|
|
66
|
+
await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
|
|
67
|
+
|
|
68
|
+
let content = this.#cache.css.get(inputPath);
|
|
69
|
+
|
|
70
|
+
if (!content) {
|
|
71
|
+
content = await fs.promises.readFile(inputPath, "utf8");
|
|
72
|
+
this.#cache.css.set(inputPath, content);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!hasTailwind) {
|
|
76
|
+
await fs.promises.writeFile(outputPath, content);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const contentHash = crypto.createHash("md5").update(content).digest("hex");
|
|
81
|
+
|
|
82
|
+
if (!this.#compiler || this.#lastCompilerHash !== contentHash) {
|
|
83
|
+
const { compile } = await import("@tailwindcss/node");
|
|
84
|
+
|
|
85
|
+
this.#compiler = await compile(content, {
|
|
86
|
+
base: this.#cssInput,
|
|
87
|
+
onDependency: () => {}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.#lastCompilerHash = contentHash;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!this.#scanner) {
|
|
94
|
+
const { Scanner } = await import("@tailwindcss/oxide");
|
|
95
|
+
|
|
96
|
+
this.#scanner = new Scanner({
|
|
97
|
+
sources: [{
|
|
98
|
+
base: this.#projectBase,
|
|
99
|
+
pattern: "resources/views/**/*.{tsx,ts,jsx,js}",
|
|
100
|
+
negated: false
|
|
101
|
+
}]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const candidates = this.#scanner.scan();
|
|
106
|
+
const css = this.#compiler.build(candidates);
|
|
107
|
+
|
|
108
|
+
if (isDev) {
|
|
109
|
+
await fs.promises.writeFile(outputPath, css);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const { optimize } = await import("@tailwindcss/node");
|
|
113
|
+
const result = optimize(css, { minify: true });
|
|
114
|
+
|
|
115
|
+
await fs.promises.writeFile(outputPath, result.code);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#detectTailwind() {
|
|
120
|
+
if (this.#hasTailwind !== null) {
|
|
121
|
+
return this.#hasTailwind;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!fs.existsSync(this.#cssInput)) {
|
|
125
|
+
this.#hasTailwind = false;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tailwindPattern = /@(import\s+["']tailwindcss["']|tailwind\s+(base|components|utilities))/;
|
|
130
|
+
|
|
131
|
+
for (const filename of fs.readdirSync(this.#cssInput).filter(f => f.endsWith(".css"))) {
|
|
132
|
+
const filePath = path.join(this.#cssInput, filename);
|
|
133
|
+
|
|
134
|
+
let content = this.#cache.css.get(filePath);
|
|
135
|
+
|
|
136
|
+
if (!content) {
|
|
137
|
+
content = fs.readFileSync(filePath, "utf8");
|
|
138
|
+
this.#cache.css.set(filePath, content);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (tailwindPattern.test(content)) {
|
|
142
|
+
this.#hasTailwind = true;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.#hasTailwind = false;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default CssBuilder;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import traverse from "@babel/traverse";
|
|
3
|
+
import { parseSync, Visitor } from "oxc-parser";
|
|
5
4
|
import Paths from "../Core/Paths.js";
|
|
6
5
|
import Layout from "../View/Layout.js";
|
|
7
6
|
import COLORS from "./colors.js";
|
|
@@ -25,13 +24,17 @@ const MAX_DEPTH = 50;
|
|
|
25
24
|
* Detects client components, extracts imports, and builds dependency graphs.
|
|
26
25
|
*/
|
|
27
26
|
class FileAnalyzer {
|
|
28
|
-
#traverse = traverse.default;
|
|
29
27
|
#cache;
|
|
28
|
+
#astCache = new Map();
|
|
30
29
|
|
|
31
30
|
constructor(cache) {
|
|
32
31
|
this.#cache = cache;
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
getAst(filePath) {
|
|
35
|
+
return this.#astCache.get(filePath) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
35
38
|
/**
|
|
36
39
|
* Discovers entry points and layouts in a directory.
|
|
37
40
|
* @param {string} baseDir - Directory to scan for TSX files.
|
|
@@ -147,13 +150,15 @@ class FileAnalyzer {
|
|
|
147
150
|
const source = fs.readFileSync(filePath, "utf8");
|
|
148
151
|
this.#cache.fileHashes.set(filePath + ":mtime", mtime);
|
|
149
152
|
|
|
150
|
-
let
|
|
153
|
+
let program;
|
|
151
154
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
const parsed = parseSync(filePath, source);
|
|
156
|
+
|
|
157
|
+
if (parsed.errors.length > 0) throw new Error(parsed.errors[0].message);
|
|
158
|
+
|
|
159
|
+
program = parsed.program;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
157
162
|
const emptyMeta = {
|
|
158
163
|
imports: [],
|
|
159
164
|
css: [],
|
|
@@ -164,10 +169,20 @@ class FileAnalyzer {
|
|
|
164
169
|
isClient: false
|
|
165
170
|
};
|
|
166
171
|
this.#cache.fileMeta.set(filePath, emptyMeta);
|
|
172
|
+
this.#astCache.delete(filePath);
|
|
167
173
|
return emptyMeta;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
|
|
176
|
+
this.#astCache.set(filePath, program);
|
|
177
|
+
|
|
178
|
+
let isClient = false;
|
|
179
|
+
|
|
180
|
+
for (const node of program.body) {
|
|
181
|
+
if (node.type === "ExpressionStatement" && node.directive === "use client") {
|
|
182
|
+
isClient = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
171
186
|
|
|
172
187
|
const meta = {
|
|
173
188
|
imports: new Set(),
|
|
@@ -181,23 +196,25 @@ class FileAnalyzer {
|
|
|
181
196
|
layoutDisabled: false
|
|
182
197
|
};
|
|
183
198
|
|
|
184
|
-
this
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
const self = this;
|
|
200
|
+
|
|
201
|
+
const visitor = new Visitor({
|
|
202
|
+
ImportDeclaration(node) {
|
|
203
|
+
const src = node.source.value;
|
|
187
204
|
|
|
188
|
-
if (
|
|
189
|
-
meta.imports.add(
|
|
205
|
+
if (src.startsWith(".")) {
|
|
206
|
+
meta.imports.add(src);
|
|
190
207
|
}
|
|
191
208
|
|
|
192
|
-
if (
|
|
193
|
-
const resolved = path.resolve(path.dirname(filePath),
|
|
209
|
+
if (src.endsWith(".css")) {
|
|
210
|
+
const resolved = path.resolve(path.dirname(filePath), src);
|
|
194
211
|
if (resolved.startsWith(Paths.project)) {
|
|
195
212
|
meta.css.add(resolved);
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
|
|
199
|
-
if (
|
|
200
|
-
for (const specifier of
|
|
216
|
+
if (src === "react") {
|
|
217
|
+
for (const specifier of node.specifiers) {
|
|
201
218
|
if (specifier.type === "ImportSpecifier" && CLIENT_HOOKS.has(specifier.imported.name)) {
|
|
202
219
|
meta.needsClient = true;
|
|
203
220
|
}
|
|
@@ -208,28 +225,32 @@ class FileAnalyzer {
|
|
|
208
225
|
}
|
|
209
226
|
},
|
|
210
227
|
|
|
211
|
-
MemberExpression
|
|
228
|
+
MemberExpression(node) {
|
|
212
229
|
if (!meta.needsClient &&
|
|
213
|
-
|
|
214
|
-
CLIENT_HOOKS.has(
|
|
230
|
+
node.object.name === meta.reactNamespace &&
|
|
231
|
+
CLIENT_HOOKS.has(node.property.name)) {
|
|
215
232
|
meta.needsClient = true;
|
|
216
233
|
}
|
|
217
234
|
},
|
|
218
235
|
|
|
219
|
-
CallExpression
|
|
236
|
+
CallExpression(node) {
|
|
220
237
|
if (!meta.needsClient &&
|
|
221
|
-
|
|
222
|
-
/^use[A-Z]/.test(
|
|
238
|
+
node.callee.type === "Identifier" &&
|
|
239
|
+
/^use[A-Z]/.test(node.callee.name)) {
|
|
223
240
|
meta.needsClient = true;
|
|
224
241
|
}
|
|
225
242
|
},
|
|
226
243
|
|
|
227
|
-
ExportNamedDeclaration
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
244
|
+
ExportNamedDeclaration(node) {
|
|
245
|
+
self.#extractNamedExports(node, meta);
|
|
246
|
+
},
|
|
247
|
+
ExportDefaultDeclaration() { meta.hasDefault = true; },
|
|
248
|
+
JSXElement() { meta.jsx = true; },
|
|
249
|
+
JSXFragment() { meta.jsx = true; }
|
|
231
250
|
});
|
|
232
251
|
|
|
252
|
+
visitor.visit(program);
|
|
253
|
+
|
|
233
254
|
// Only enforce "use client" requirement on .tsx files.
|
|
234
255
|
// .ts files (hooks, utils) are pure logic modules — they don't define components
|
|
235
256
|
// and are safe to import from either server or client code.
|
|
@@ -251,11 +272,7 @@ class FileAnalyzer {
|
|
|
251
272
|
return result;
|
|
252
273
|
}
|
|
253
274
|
|
|
254
|
-
#extractNamedExports(
|
|
255
|
-
const node = path.node;
|
|
256
|
-
|
|
257
|
-
// Re-exports: export { X } from './Y' — track the source as an import
|
|
258
|
-
// so the dependency graph stays complete through barrel files.
|
|
275
|
+
#extractNamedExports(node, meta) {
|
|
259
276
|
if (node.source && node.source.value && node.source.value.startsWith(".")) {
|
|
260
277
|
meta.imports.add(node.source.value);
|
|
261
278
|
}
|
|
@@ -266,7 +283,7 @@ class FileAnalyzer {
|
|
|
266
283
|
for (const decl of declaration.declarations) {
|
|
267
284
|
if (decl.id.type === "Identifier") {
|
|
268
285
|
if (decl.id.name === "Layout" &&
|
|
269
|
-
decl.init?.type === "
|
|
286
|
+
decl.init?.type === "Literal" &&
|
|
270
287
|
decl.init.value === false) {
|
|
271
288
|
meta.layoutDisabled = true;
|
|
272
289
|
}
|