@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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const COLORS = {
4
4
  reset: "\x1b[0m",
@@ -1,136 +1,152 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import crypto from "crypto";
4
- import postcss from "postcss";
5
- import tailwindPostcss from "@tailwindcss/postcss";
6
-
7
- /**
8
- * Handles CSS file building with Tailwind CSS support.
9
- * Processes CSS files, handles caching, and detects Tailwind usage.
10
- */
11
- class CssBuilder {
12
- #cache;
13
- #cssInput;
14
- #cssOutput;
15
- #hasTailwind = null;
16
-
17
- constructor(cache, cssInput, cssOutput) {
18
- this.#cache = cache;
19
- this.#cssInput = cssInput;
20
- this.#cssOutput = cssOutput;
21
- }
22
-
23
- /**
24
- * Builds all CSS files in the input directory.
25
- * @param {boolean} viewsChanged - Whether views have changed (forces Tailwind rebuild).
26
- * @returns {Promise<number>} Number of CSS files processed.
27
- */
28
- async build(viewsChanged, isDev = false) {
29
- if (!fs.existsSync(this.#cssInput)) {
30
- return 0;
31
- }
32
-
33
- const cssFiles = fs.readdirSync(this.#cssInput).filter(f => f.endsWith(".css"));
34
- if (!cssFiles.length) {
35
- return 0;
36
- }
37
-
38
- const hasTailwind = this.#detectTailwind();
39
- const filesToProcess = [];
40
-
41
- for (const filename of cssFiles) {
42
- const filePath = path.join(this.#cssInput, filename);
43
- const content = await fs.promises.readFile(filePath, "utf8");
44
- const hash = crypto.createHash("md5").update(content).digest("hex");
45
- const cachedHash = this.#cache.cssHashes.get(filePath);
46
- const outputPath = path.join(this.#cssOutput, filename);
47
- const outputExists = fs.existsSync(outputPath);
48
-
49
- if (cachedHash !== hash || !outputExists) {
50
- this.#cache.css.set(filePath, content);
51
- this.#cache.cssHashes.set(filePath, hash);
52
- filesToProcess.push(filename);
53
- }
54
- }
55
-
56
- // If Tailwind is used and views changed, force rebuild all CSS
57
- if (hasTailwind && viewsChanged && !filesToProcess.length) {
58
- filesToProcess.push(...cssFiles);
59
- }
60
-
61
- if (!filesToProcess.length) return 0;
62
-
63
- if (hasTailwind && !this.#cache.tailwindProcessor) {
64
- this.#cache.tailwindProcessor = postcss([tailwindPostcss()]);
65
- }
66
- const processor = hasTailwind ? this.#cache.tailwindProcessor : null;
67
-
68
- await Promise.all(filesToProcess.map(filename => this.#processCss(filename, hasTailwind, processor, isDev)));
69
-
70
- return filesToProcess.length;
71
- }
72
-
73
- async #processCss(filename, hasTailwind, processor, isDev) {
74
- const inputPath = path.join(this.#cssInput, filename);
75
- const outputPath = path.join(this.#cssOutput, filename);
76
-
77
- await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
78
-
79
- let content = this.#cache.css.get(inputPath);
80
- if (!content) {
81
- content = await fs.promises.readFile(inputPath, "utf8");
82
- this.#cache.css.set(inputPath, content);
83
- }
84
-
85
- if (!hasTailwind) {
86
- await fs.promises.writeFile(outputPath, content);
87
- return;
88
- }
89
-
90
- const result = await processor.process(content, {
91
- from: inputPath,
92
- to: outputPath,
93
- map: isDev ? { inline: false } : false
94
- });
95
-
96
- await fs.promises.writeFile(outputPath, result.css);
97
-
98
- if (result.map) {
99
- await fs.promises.writeFile(`${outputPath}.map`, result.map.toString());
100
- }
101
- }
102
-
103
- #detectTailwind() {
104
- // Return cached result if available
105
- if (this.#hasTailwind !== null) {
106
- return this.#hasTailwind;
107
- }
108
-
109
- if (!fs.existsSync(this.#cssInput)) {
110
- this.#hasTailwind = false;
111
- return false;
112
- }
113
-
114
- const tailwindPattern = /@(import\s+["']tailwindcss["']|tailwind\s+(base|components|utilities))/;
115
-
116
- for (const filename of fs.readdirSync(this.#cssInput).filter(f => f.endsWith(".css"))) {
117
- const filePath = path.join(this.#cssInput, filename);
118
-
119
- let content = this.#cache.css.get(filePath);
120
- if (!content) {
121
- content = fs.readFileSync(filePath, "utf8");
122
- this.#cache.css.set(filePath, content);
123
- }
124
-
125
- if (tailwindPattern.test(content)) {
126
- this.#hasTailwind = true;
127
- return true;
128
- }
129
- }
130
-
131
- this.#hasTailwind = false;
132
- return false;
133
- }
134
- }
135
-
136
- export default CssBuilder;
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 { parse } from "@babel/parser";
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 ast;
153
+ let program;
151
154
  try {
152
- ast = parse(source, {
153
- sourceType: "module",
154
- plugins: ["typescript", "jsx"]
155
- });
156
- } catch {
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
- const isClient = ast.program.directives?.some(d => d.value?.value === "use client");
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.#traverse(ast, {
185
- ImportDeclaration: (p) => {
186
- const source = p.node.source.value;
199
+ const self = this;
200
+
201
+ const visitor = new Visitor({
202
+ ImportDeclaration(node) {
203
+ const src = node.source.value;
187
204
 
188
- if (source.startsWith(".")) {
189
- meta.imports.add(source);
205
+ if (src.startsWith(".")) {
206
+ meta.imports.add(src);
190
207
  }
191
208
 
192
- if (source.endsWith(".css")) {
193
- const resolved = path.resolve(path.dirname(filePath), source);
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 (source === "react") {
200
- for (const specifier of p.node.specifiers) {
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: (p) => {
228
+ MemberExpression(node) {
212
229
  if (!meta.needsClient &&
213
- p.node.object.name === meta.reactNamespace &&
214
- CLIENT_HOOKS.has(p.node.property.name)) {
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: (p) => {
236
+ CallExpression(node) {
220
237
  if (!meta.needsClient &&
221
- p.node.callee.type === "Identifier" &&
222
- /^use[A-Z]/.test(p.node.callee.name)) {
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: (p) => this.#extractNamedExports(p, meta),
228
- ExportDefaultDeclaration: () => { meta.hasDefault = true; },
229
- JSXElement: () => { meta.jsx = true; },
230
- JSXFragment: () => { meta.jsx = true; }
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(path, meta) {
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 === "BooleanLiteral" &&
286
+ decl.init?.type === "Literal" &&
270
287
  decl.init.value === false) {
271
288
  meta.layoutDisabled = true;
272
289
  }