@nitronjs/framework 0.1.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 (87) hide show
  1. package/README.md +429 -0
  2. package/cli/create.js +260 -0
  3. package/cli/njs.js +164 -0
  4. package/lib/Auth/Manager.js +111 -0
  5. package/lib/Build/Manager.js +1232 -0
  6. package/lib/Console/Commands/BuildCommand.js +25 -0
  7. package/lib/Console/Commands/DevCommand.js +385 -0
  8. package/lib/Console/Commands/MakeCommand.js +110 -0
  9. package/lib/Console/Commands/MigrateCommand.js +98 -0
  10. package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
  11. package/lib/Console/Commands/SeedCommand.js +92 -0
  12. package/lib/Console/Commands/StorageLinkCommand.js +31 -0
  13. package/lib/Console/Stubs/controller.js +19 -0
  14. package/lib/Console/Stubs/middleware.js +9 -0
  15. package/lib/Console/Stubs/migration.js +23 -0
  16. package/lib/Console/Stubs/model.js +7 -0
  17. package/lib/Console/Stubs/page-hydration.tsx +54 -0
  18. package/lib/Console/Stubs/seeder.js +9 -0
  19. package/lib/Console/Stubs/vendor.tsx +11 -0
  20. package/lib/Core/Config.js +86 -0
  21. package/lib/Core/Environment.js +21 -0
  22. package/lib/Core/Paths.js +188 -0
  23. package/lib/Database/Connection.js +61 -0
  24. package/lib/Database/DB.js +84 -0
  25. package/lib/Database/Drivers/MySQLDriver.js +234 -0
  26. package/lib/Database/Manager.js +162 -0
  27. package/lib/Database/Model.js +161 -0
  28. package/lib/Database/QueryBuilder.js +714 -0
  29. package/lib/Database/QueryValidation.js +62 -0
  30. package/lib/Database/Schema/Blueprint.js +126 -0
  31. package/lib/Database/Schema/Manager.js +116 -0
  32. package/lib/Date/DateTime.js +108 -0
  33. package/lib/Date/Locale.js +68 -0
  34. package/lib/Encryption/Manager.js +47 -0
  35. package/lib/Filesystem/Manager.js +49 -0
  36. package/lib/Hashing/Manager.js +25 -0
  37. package/lib/Http/Server.js +317 -0
  38. package/lib/Logging/Manager.js +153 -0
  39. package/lib/Mail/Manager.js +120 -0
  40. package/lib/Route/Loader.js +81 -0
  41. package/lib/Route/Manager.js +265 -0
  42. package/lib/Runtime/Entry.js +11 -0
  43. package/lib/Session/File.js +299 -0
  44. package/lib/Session/Manager.js +259 -0
  45. package/lib/Session/Memory.js +67 -0
  46. package/lib/Session/Session.js +196 -0
  47. package/lib/Support/Str.js +100 -0
  48. package/lib/Translation/Manager.js +49 -0
  49. package/lib/Validation/MimeTypes.js +39 -0
  50. package/lib/Validation/Validator.js +691 -0
  51. package/lib/View/Manager.js +544 -0
  52. package/lib/View/Templates/default/Home.tsx +262 -0
  53. package/lib/View/Templates/default/MainLayout.tsx +44 -0
  54. package/lib/View/Templates/errors/404.tsx +13 -0
  55. package/lib/View/Templates/errors/500.tsx +13 -0
  56. package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
  57. package/lib/View/Templates/messages/Maintenance.tsx +17 -0
  58. package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
  59. package/lib/index.js +57 -0
  60. package/package.json +47 -0
  61. package/skeleton/.env.example +26 -0
  62. package/skeleton/app/Controllers/HomeController.js +9 -0
  63. package/skeleton/app/Kernel.js +11 -0
  64. package/skeleton/app/Middlewares/Authentication.js +9 -0
  65. package/skeleton/app/Middlewares/Guest.js +9 -0
  66. package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
  67. package/skeleton/app/Models/User.js +7 -0
  68. package/skeleton/config/app.js +4 -0
  69. package/skeleton/config/auth.js +16 -0
  70. package/skeleton/config/database.js +27 -0
  71. package/skeleton/config/hash.js +3 -0
  72. package/skeleton/config/server.js +28 -0
  73. package/skeleton/config/session.js +21 -0
  74. package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
  75. package/skeleton/database/seeders/UserSeeder.js +15 -0
  76. package/skeleton/globals.d.ts +1 -0
  77. package/skeleton/package.json +24 -0
  78. package/skeleton/public/.gitkeep +0 -0
  79. package/skeleton/resources/css/.gitkeep +0 -0
  80. package/skeleton/resources/langs/.gitkeep +0 -0
  81. package/skeleton/resources/views/Site/Home.tsx +66 -0
  82. package/skeleton/routes/web.js +4 -0
  83. package/skeleton/storage/app/private/.gitkeep +0 -0
  84. package/skeleton/storage/app/public/.gitkeep +0 -0
  85. package/skeleton/storage/framework/sessions/.gitkeep +0 -0
  86. package/skeleton/storage/logs/.gitkeep +0 -0
  87. package/skeleton/tsconfig.json +33 -0
@@ -0,0 +1,1232 @@
1
+ #!/usr/bin/env node
2
+ import dotenv from "dotenv";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import esbuild from "esbuild";
6
+ import { parse } from "@babel/parser";
7
+ import traverse from "@babel/traverse";
8
+ import postcss from "postcss";
9
+ import tailwindPostcss from "@tailwindcss/postcss";
10
+ import Paths from "../Core/Paths.js";
11
+
12
+ dotenv.config({ quiet: true });
13
+
14
+ const COLORS = {
15
+ reset: "\x1b[0m",
16
+ dim: "\x1b[2m",
17
+ red: "\x1b[31m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ cyan: "\x1b[36m"
21
+ };
22
+
23
+ const CLIENT_HOOKS = new Set([
24
+ "useState",
25
+ "useEffect",
26
+ "useRef",
27
+ "useReducer",
28
+ "useLayoutEffect",
29
+ "useCallback",
30
+ "useMemo"
31
+ ]);
32
+
33
+ const MAX_DEPTH = 50;
34
+
35
+ function sanitizeName(name) {
36
+ return name.replace(/[^a-zA-Z0-9_$]/g, "_");
37
+ }
38
+
39
+ const JSX_RUNTIME = `
40
+ import * as React from 'react';
41
+ import * as OriginalJsx from '__react_jsx_original__';
42
+
43
+ const CTX = Symbol.for('__nitron_view_context__');
44
+ const MARK = Symbol.for('__nitron_client_component__');
45
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
46
+
47
+ function getContext() {
48
+ return globalThis[CTX]?.getStore?.();
49
+ }
50
+
51
+ globalThis.csrf = () => getContext()?.csrf || '';
52
+
53
+ globalThis.request = () => {
54
+ const ctx = getContext();
55
+ return ctx?.request || { params: {}, query: {}, url: '', method: 'GET', headers: {} };
56
+ };
57
+
58
+ const DepthContext = React.createContext(false);
59
+ const componentCache = new WeakMap();
60
+
61
+ function sanitizeProps(obj, seen = new WeakSet()) {
62
+ if (obj == null) return obj;
63
+
64
+ const type = typeof obj;
65
+ if (type === 'function' || type === 'symbol') return undefined;
66
+ if (type === 'bigint') return obj.toString();
67
+ if (type !== 'object') return obj;
68
+
69
+ if (seen.has(obj)) return undefined;
70
+ seen.add(obj);
71
+
72
+ if (Array.isArray(obj)) {
73
+ return obj.map(item => {
74
+ const sanitized = sanitizeProps(item, seen);
75
+ return sanitized === undefined ? null : sanitized;
76
+ });
77
+ }
78
+
79
+ if (obj instanceof Date) return obj.toISOString();
80
+ if (obj._attributes && typeof obj._attributes === 'object') {
81
+ return sanitizeProps(obj._attributes, seen);
82
+ }
83
+ if (typeof obj.toJSON === 'function') {
84
+ return sanitizeProps(obj.toJSON(), seen);
85
+ }
86
+
87
+ const proto = Object.getPrototypeOf(obj);
88
+ if (proto !== Object.prototype && proto !== null) return undefined;
89
+
90
+ const result = {};
91
+ for (const key of Object.keys(obj)) {
92
+ if (UNSAFE_KEYS.has(key)) continue;
93
+ const value = sanitizeProps(obj[key], seen);
94
+ if (value !== undefined) result[key] = value;
95
+ }
96
+ return result;
97
+ }
98
+
99
+ function wrapWithDepth(children) {
100
+ return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
101
+ }
102
+
103
+ function createIsland(Component, name) {
104
+ function IslandBoundary(props) {
105
+ if (React.useContext(DepthContext)) {
106
+ return OriginalJsx.jsx(Component, props);
107
+ }
108
+
109
+ const id = React.useId();
110
+ const safeProps = sanitizeProps(props) || {};
111
+
112
+ const ctx = getContext();
113
+ if (ctx) {
114
+ ctx.props = ctx.props || {};
115
+ ctx.props[id] = safeProps;
116
+ }
117
+
118
+ return OriginalJsx.jsx('div', {
119
+ 'data-cid': id,
120
+ 'data-island': name,
121
+ children: wrapWithDepth(OriginalJsx.jsx(Component, props))
122
+ });
123
+ }
124
+
125
+ IslandBoundary.displayName = 'Island(' + name + ')';
126
+ return IslandBoundary;
127
+ }
128
+
129
+ function getWrappedComponent(Component) {
130
+ if (!componentCache.has(Component)) {
131
+ const name = Component.displayName || Component.name || 'Anonymous';
132
+ componentCache.set(Component, createIsland(Component, name));
133
+ }
134
+ return componentCache.get(Component);
135
+ }
136
+
137
+ export function jsx(type, props, key) {
138
+ if (typeof type === 'function' && type[MARK]) {
139
+ return OriginalJsx.jsx(getWrappedComponent(type), props, key);
140
+ }
141
+ return OriginalJsx.jsx(type, props, key);
142
+ }
143
+
144
+ export function jsxs(type, props, key) {
145
+ if (typeof type === 'function' && type[MARK]) {
146
+ return OriginalJsx.jsx(getWrappedComponent(type), props, key);
147
+ }
148
+ return OriginalJsx.jsxs(type, props, key);
149
+ }
150
+
151
+ export const Fragment = OriginalJsx.Fragment;
152
+ `;
153
+
154
+ class Builder {
155
+ #isDev = process.env.APP_DEV === "true";
156
+ #traverse = traverse.default;
157
+ #manifest = {};
158
+ #paths;
159
+ #stats = { user: 0, framework: 0, islands: 0, css: 0 };
160
+ #cache = { imports: new Map(), css: new Map(), hydrationTemplate: null };
161
+
162
+ constructor() {
163
+ this.#paths = {
164
+ // User project paths
165
+ userViews: Paths.views,
166
+ userOutput: Paths.buildViews,
167
+ frameworkOutput: Paths.buildFrameworkViews,
168
+ cssInput: Paths.css,
169
+ cssOutput: Paths.publicCss,
170
+ jsOutput: Paths.publicJs,
171
+ jsxRuntime: Paths.jsxRuntime,
172
+ nitronTemp: Paths.nitronTemp,
173
+ // Framework paths
174
+ frameworkViews: Paths.frameworkViews,
175
+ templates: Paths.frameworkTemplates
176
+ };
177
+ }
178
+
179
+ async run(only = null) {
180
+ const startTime = Date.now();
181
+
182
+ try {
183
+ console.log(`\n${COLORS.cyan}⚡ NitronJS Build${COLORS.reset}\n`);
184
+
185
+ if (!only) {
186
+ this.#cleanOutputDirs();
187
+ }
188
+
189
+ this.#writeJsxRuntime();
190
+
191
+ await Promise.all([
192
+ (!only || only === "views") ? this.#buildViews() : null,
193
+ (!only || only === "css") ? this.#buildCss() : null
194
+ ]);
195
+
196
+ this.#cleanupTemp();
197
+ this.#printSummary(Date.now() - startTime);
198
+ return true;
199
+ } catch (error) {
200
+ this.#cleanupTemp();
201
+ console.log(`\n${COLORS.red}✖ Build failed: ${error.message}${COLORS.reset}\n`);
202
+ if (this.#isDev) console.error(error);
203
+ return false;
204
+ }
205
+ }
206
+
207
+ #printSummary(duration) {
208
+ const { user, framework, islands, css } = this.#stats;
209
+ const lines = [];
210
+
211
+ if (user || framework) {
212
+ const parts = [];
213
+ if (user) parts.push(`${user} user`);
214
+ if (framework) parts.push(`${framework} framework`);
215
+ lines.push(` ${COLORS.green}✔${COLORS.reset} Views ${parts.join(", ")}`);
216
+ }
217
+
218
+ if (islands) {
219
+ const plural = islands > 1 ? "s" : "";
220
+ lines.push(` ${COLORS.green}✔${COLORS.reset} Hydration ${islands} bundle${plural}`);
221
+ }
222
+
223
+ if (css) {
224
+ const plural = css > 1 ? "s" : "";
225
+ lines.push(` ${COLORS.green}✔${COLORS.reset} CSS ${css} stylesheet${plural}`);
226
+ }
227
+
228
+ console.log(lines.join("\n"));
229
+ console.log(`\n${COLORS.dim}Completed in ${duration}ms${COLORS.reset}\n`);
230
+ }
231
+
232
+ async #buildViews() {
233
+ const [, userBundle, frameworkBundle] = await Promise.all([
234
+ this.#buildVendor(),
235
+ this.#buildViewBundle("user", this.#paths.userViews, this.#paths.userOutput),
236
+ this.#buildViewBundle("framework", this.#paths.frameworkViews, this.#paths.frameworkOutput)
237
+ ]);
238
+
239
+ await this.#buildHydrationBundles(userBundle, frameworkBundle);
240
+ this.#writeManifest();
241
+ }
242
+
243
+ async #buildVendor() {
244
+ await esbuild.build({
245
+ entryPoints: [path.join(this.#paths.templates, "vendor.tsx")],
246
+ outfile: path.join(this.#paths.jsOutput, "vendor.js"),
247
+ bundle: true,
248
+ platform: "browser",
249
+ format: "iife",
250
+ target: "es2020",
251
+ sourcemap: this.#isDev,
252
+ minify: !this.#isDev,
253
+ jsx: "automatic"
254
+ });
255
+ }
256
+
257
+ async #buildViewBundle(namespace, srcDir, outDir) {
258
+ if (!fs.existsSync(srcDir)) {
259
+ return { entries: [], meta: new Map(), srcDir, namespace };
260
+ }
261
+
262
+ const { entries, meta } = this.#discoverEntries(srcDir);
263
+
264
+ if (!entries.length) {
265
+ return { entries: [], meta: new Map(), srcDir, namespace };
266
+ }
267
+
268
+ this.#addToManifest(entries, meta, srcDir, namespace);
269
+ await this.#runEsbuild(entries, outDir, { meta, outbase: srcDir });
270
+ await this.#postProcessMeta(entries, srcDir, outDir);
271
+
272
+ this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
273
+
274
+ return { entries, meta, srcDir, namespace };
275
+ }
276
+
277
+ async #postProcessMeta(entries, srcDir, outDir) {
278
+ const getExportedName = (specifier) => {
279
+ const match = specifier.match(/^.+\s+as\s+(\w+)$/);
280
+ return match ? match[1] : specifier.trim();
281
+ };
282
+
283
+ const processEntry = async (entry) => {
284
+ const relativePath = path.relative(srcDir, entry).replace(/\.tsx$/, ".js");
285
+ const outputFile = path.join(outDir, relativePath);
286
+
287
+ let code;
288
+ try {
289
+ code = await fs.promises.readFile(outputFile, "utf8");
290
+ } catch {
291
+ return;
292
+ }
293
+
294
+ if (code.includes("__mergedMeta__")) {
295
+ return;
296
+ }
297
+
298
+ const metaMatches = [...code.matchAll(/\b(?:var|let|const)\s+(Meta\d*)\s*=\s*\{/g)];
299
+ const metaNames = metaMatches.map(m => m[1]);
300
+
301
+ const exportMatches = [...code.matchAll(/export\s*\{([\s\S]*?)\};/g)];
302
+ if (!exportMatches.length) {
303
+ return;
304
+ }
305
+
306
+ const lastExport = exportMatches[exportMatches.length - 1];
307
+ const exportStart = lastExport.index;
308
+ const exportLength = lastExport[0].length;
309
+ const exportInner = lastExport[1];
310
+
311
+ const seenNames = new Set();
312
+ const exportItems = [];
313
+
314
+ for (const item of exportInner.split(",").map(s => s.trim()).filter(Boolean)) {
315
+ const exportedName = getExportedName(item);
316
+ if (!seenNames.has(exportedName)) {
317
+ seenNames.add(exportedName);
318
+ exportItems.push(item);
319
+ }
320
+ }
321
+
322
+ const hasMetaExport = seenNames.has("Meta");
323
+
324
+ if (metaNames.length >= 2) {
325
+ const mergeCode = `var __mergedMeta__=(()=>{var r={};[${metaNames.join(",")}].forEach(m=>{if(!m)return;Object.keys(m).forEach(k=>{if(k==='title'){var p=r.title,c=m.title;r.title=p&&typeof p==='object'&&p.template&&c?p.template.replace('%s',typeof c==='object'?c.default:c):c}else if(!(k in r))r[k]=m[k]})});return r})();\n`;
326
+
327
+ const finalItems = exportItems.filter(item => getExportedName(item) !== "Meta");
328
+ finalItems.unshift("__mergedMeta__ as Meta");
329
+
330
+ const newExport = `export {\n ${finalItems.join(",\n ")}\n};`;
331
+ code = code.slice(0, exportStart) + mergeCode + newExport + code.slice(exportStart + exportLength);
332
+
333
+ await fs.promises.writeFile(outputFile, code);
334
+ return;
335
+ }
336
+
337
+ if (metaNames.length === 1 && !hasMetaExport) {
338
+ exportItems.unshift(`${metaNames[0]} as Meta`);
339
+ }
340
+
341
+ const newExport = `export {\n ${exportItems.join(",\n ")}\n};`;
342
+ code = code.slice(0, exportStart) + newExport + code.slice(exportStart + exportLength);
343
+
344
+ await fs.promises.writeFile(outputFile, code);
345
+ };
346
+
347
+ await Promise.all(entries.map(processEntry));
348
+ }
349
+
350
+ async #buildHydrationBundles(userBundle, frameworkBundle) {
351
+ const hydrationFiles = [];
352
+
353
+ for (const bundle of [userBundle, frameworkBundle]) {
354
+ if (!bundle?.entries.length) continue;
355
+
356
+ for (const viewPath of bundle.entries) {
357
+ const clientComponents = this.#findClientComponents(viewPath, bundle.meta, new Set());
358
+
359
+ if (clientComponents.size) {
360
+ const hydrationFile = this.#generateHydrationFile(
361
+ viewPath,
362
+ bundle.srcDir,
363
+ clientComponents,
364
+ bundle.meta,
365
+ bundle.namespace
366
+ );
367
+ hydrationFiles.push(hydrationFile);
368
+ }
369
+ }
370
+ }
371
+
372
+ if (!hydrationFiles.length) {
373
+ return;
374
+ }
375
+
376
+ await this.#runEsbuild(hydrationFiles, this.#paths.jsOutput, {
377
+ platform: "browser",
378
+ target: "es2020",
379
+ outbase: path.join(Paths.project, ".nitron/hydration"),
380
+ external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"],
381
+ vendor: true,
382
+ serverFunctions: true
383
+ });
384
+
385
+ this.#stats.islands = hydrationFiles.length;
386
+ }
387
+
388
+ #findClientComponents(file, meta, seen, depth = 0) {
389
+ const result = new Set();
390
+
391
+ if (depth > MAX_DEPTH || seen.has(file)) {
392
+ return result;
393
+ }
394
+ seen.add(file);
395
+
396
+ const fileMeta = meta.get(file);
397
+ if (!fileMeta) {
398
+ return result;
399
+ }
400
+
401
+ if (fileMeta.isClient) {
402
+ result.add(file);
403
+ }
404
+
405
+ for (const importPath of fileMeta.imports) {
406
+ const resolvedPath = this.#resolveImport(file, importPath);
407
+
408
+ if (meta.has(resolvedPath)) {
409
+ const childComponents = this.#findClientComponents(resolvedPath, meta, seen, depth + 1);
410
+ for (const component of childComponents) {
411
+ result.add(component);
412
+ }
413
+ }
414
+ }
415
+
416
+ return result;
417
+ }
418
+
419
+ #generateHydrationFile(viewPath, srcDir, clientComponents, meta, namespace) {
420
+ if (!this.#cache.hydrationTemplate) {
421
+ const templatePath = path.join(this.#paths.templates, "page-hydration.tsx");
422
+ this.#cache.hydrationTemplate = fs.readFileSync(templatePath, "utf8");
423
+ }
424
+
425
+ const viewRelative = path.relative(srcDir, viewPath).replace(/\.tsx$/, "").toLowerCase();
426
+ const outputDir = path.join(Paths.project, ".nitron/hydration", path.dirname(viewRelative));
427
+ const outputFile = path.join(outputDir, path.basename(viewRelative) + ".tsx");
428
+
429
+ fs.mkdirSync(outputDir, { recursive: true });
430
+
431
+ const imports = [];
432
+ const manifestEntries = [];
433
+ let index = 0;
434
+
435
+ for (const componentPath of clientComponents) {
436
+ const componentMeta = meta.get(componentPath);
437
+ if (!componentMeta) continue;
438
+
439
+ const baseName = path.basename(componentPath, ".tsx");
440
+ const relativePath = path.relative(outputDir, componentPath)
441
+ .replace(/\\/g, "/")
442
+ .replace(/\.tsx$/, "");
443
+
444
+ if (componentMeta.hasDefault) {
445
+ const importName = sanitizeName(baseName) + "_" + index++;
446
+ imports.push(`import ${importName} from "${relativePath}";`);
447
+ manifestEntries.push(` "${baseName}": ${importName}`);
448
+ }
449
+
450
+ for (const namedExport of componentMeta.named || []) {
451
+ const importName = sanitizeName(namedExport) + "_" + index++;
452
+ imports.push(`import { ${namedExport} as ${importName} } from "${relativePath}";`);
453
+ manifestEntries.push(` "${namedExport}": ${importName}`);
454
+ }
455
+ }
456
+
457
+ const code = this.#cache.hydrationTemplate
458
+ .replace("// __COMPONENT_IMPORTS__", imports.join("\n"))
459
+ .replace(
460
+ "// __COMPONENT_MANIFEST__",
461
+ `Object.assign(componentManifest, {\n${manifestEntries.join(",\n")}\n});`
462
+ );
463
+
464
+ fs.writeFileSync(outputFile, code);
465
+
466
+ const manifestKey = `${namespace}:${viewRelative.replace(/\\/g, "/")}`;
467
+ if (this.#manifest[manifestKey]) {
468
+ this.#manifest[manifestKey].hydrationScript = `/js/${viewRelative.replace(/\\/g, "/")}.js`;
469
+ }
470
+
471
+ return outputFile;
472
+ }
473
+
474
+ async #buildCss() {
475
+ if (!fs.existsSync(this.#paths.cssInput)) {
476
+ return;
477
+ }
478
+
479
+ const cssFiles = fs.readdirSync(this.#paths.cssInput).filter(f => f.endsWith(".css"));
480
+ if (!cssFiles.length) {
481
+ return;
482
+ }
483
+
484
+ await Promise.all(cssFiles.map(async (filename) => {
485
+ const filePath = path.join(this.#paths.cssInput, filename);
486
+ if (!this.#cache.css.has(filePath)) {
487
+ const content = await fs.promises.readFile(filePath, "utf8");
488
+ this.#cache.css.set(filePath, content);
489
+ }
490
+ }));
491
+
492
+ const hasTailwind = this.#detectTailwind();
493
+ const processor = hasTailwind ? postcss([tailwindPostcss()]) : null;
494
+
495
+ await Promise.all(cssFiles.map(filename => this.#processCss(filename, hasTailwind, processor)));
496
+
497
+ this.#stats.css = cssFiles.length;
498
+ }
499
+
500
+ async #runEsbuild(entries, outDir, options = {}) {
501
+ const plugins = [
502
+ this.#createCssStubPlugin(),
503
+ this.#createMarkerPlugin(options),
504
+ this.#createPathAliasPlugin()
505
+ ];
506
+
507
+ if (options.vendor) {
508
+ plugins.push(this.#createVendorGlobalsPlugin());
509
+ plugins.push(this.#createServerStubPlugin());
510
+ }
511
+
512
+ if (options.serverFunctions) {
513
+ plugins.push(this.#createServerFunctionsPlugin());
514
+ }
515
+
516
+ const isNode = (options.platform ?? "node") === "node";
517
+
518
+ const config = {
519
+ entryPoints: entries,
520
+ outdir: outDir,
521
+ outbase: options.outbase,
522
+ bundle: true,
523
+ platform: options.platform ?? "node",
524
+ format: "esm",
525
+ jsx: "automatic",
526
+ target: options.target ?? "node20",
527
+ sourcemap: this.#isDev,
528
+ minify: !this.#isDev,
529
+ external: options.external ?? ["react", "react-dom", "react-dom/server"],
530
+ plugins
531
+ };
532
+
533
+ if (isNode) {
534
+ config.packages = "external";
535
+ config.treeShaking = false;
536
+ }
537
+
538
+ if (options.platform !== "browser") {
539
+ config.alias = { "react/jsx-runtime": this.#paths.jsxRuntime };
540
+ plugins.push(this.#createOriginalJsxPlugin());
541
+ }
542
+
543
+ await esbuild.build(config);
544
+ }
545
+
546
+ #createPathAliasPlugin() {
547
+ const root = Paths.project;
548
+ return {
549
+ name: "path-alias",
550
+ setup: (build) => {
551
+ // Handle @/* alias -> ./app/* with high priority
552
+ // Using filter with higher specificity to run before packages: "external"
553
+ build.onResolve({ filter: /^@\// }, async (args) => {
554
+ const relativePath = args.path.replace(/^@\//, "");
555
+ const absolutePath = path.join(root, "app", relativePath);
556
+
557
+ // Try with .js extension first, then without
558
+ const extensions = [".js", ".ts", ".jsx", ".tsx", ""];
559
+ for (const ext of extensions) {
560
+ const fullPath = absolutePath + ext;
561
+ if (fs.existsSync(fullPath)) {
562
+ // Return with explicit namespace to ensure it's bundled
563
+ return { path: fullPath, external: false };
564
+ }
565
+ }
566
+
567
+ // If it's a directory, try index files
568
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
569
+ for (const ext of [".js", ".ts", ".jsx", ".tsx"]) {
570
+ const indexPath = path.join(absolutePath, "index" + ext);
571
+ if (fs.existsSync(indexPath)) {
572
+ return { path: indexPath, external: false };
573
+ }
574
+ }
575
+ }
576
+
577
+ return { path: absolutePath + ".js", external: false };
578
+ });
579
+ }
580
+ };
581
+ }
582
+
583
+ #createOriginalJsxPlugin() {
584
+ return {
585
+ name: "original-jsx",
586
+ setup: (build) => {
587
+ build.onResolve({ filter: /^__react_jsx_original__$/ }, () => ({
588
+ path: "react/jsx-runtime",
589
+ external: true
590
+ }));
591
+ }
592
+ };
593
+ }
594
+
595
+ #createVendorGlobalsPlugin() {
596
+ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
597
+
598
+ const packages = {
599
+ "react": "__NITRON_REACT__",
600
+ "react-dom": "__NITRON_REACT_DOM__",
601
+ "react-dom/client": "__NITRON_REACT_DOM_CLIENT__",
602
+ "react/jsx-runtime": "__NITRON_JSX_RUNTIME__"
603
+ };
604
+
605
+ const patterns = Object.entries(packages).map(([pkg, global]) => ({
606
+ filter: new RegExp(`^${escapeRegex(pkg)}$`),
607
+ pkg,
608
+ global
609
+ }));
610
+
611
+ return {
612
+ name: "vendor-globals",
613
+ setup: (build) => {
614
+ for (const { filter, pkg, global } of patterns) {
615
+ build.onResolve({ filter }, () => ({
616
+ path: pkg,
617
+ namespace: "vendor-global"
618
+ }));
619
+
620
+ build.onLoad({ filter, namespace: "vendor-global" }, () => ({
621
+ contents: `module.exports = window.${global};`,
622
+ loader: "js"
623
+ }));
624
+ }
625
+ }
626
+ };
627
+ }
628
+
629
+ #createServerStubPlugin() {
630
+ return {
631
+ name: "server-stub",
632
+ setup: (build) => {
633
+ build.onResolve({ filter: /lib\/Storage\.js$/ }, (args) => ({
634
+ path: args.path,
635
+ namespace: "storage-stub"
636
+ }));
637
+
638
+ build.onLoad({ filter: /.*/, namespace: "storage-stub" }, () => ({
639
+ contents: `export default { url: p => '/storage/' + (p.startsWith('/') ? p.slice(1) : p) };`,
640
+ loader: "js"
641
+ }));
642
+
643
+ build.onResolve(
644
+ { filter: /lib\/(DB|Mail|Log|Hash|Environment|Server|Model|Validator)\.js$/ },
645
+ (args) => ({ path: args.path, namespace: "server-only" })
646
+ );
647
+
648
+ build.onLoad({ filter: /.*/, namespace: "server-only" }, (args) => {
649
+ const moduleName = args.path.split("/").pop()?.replace(".js", "") || "Module";
650
+ return {
651
+ contents: `const err = () => { throw new Error("${moduleName} is server-only") }; export default new Proxy({}, { get: err, apply: err });`,
652
+ loader: "js"
653
+ };
654
+ });
655
+ }
656
+ };
657
+ }
658
+
659
+ #createServerFunctionsPlugin() {
660
+ return {
661
+ name: "server-functions",
662
+ setup: (build) => {
663
+ build.onLoad({ filter: /\.(tsx?|jsx?)$/ }, async (args) => {
664
+ if (args.path.includes("node_modules")) {
665
+ return null;
666
+ }
667
+
668
+ let source = await fs.promises.readFile(args.path, "utf8");
669
+
670
+ if (!source.includes("csrf(") && !source.includes("route(")) {
671
+ return null;
672
+ }
673
+
674
+ source = source.replace(
675
+ /\bcsrf\s*\(\s*\)/g,
676
+ "window.__NITRON_RUNTIME__.csrf"
677
+ );
678
+
679
+ source = source.replace(
680
+ /\broute\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
681
+ (_, routeName) => `window.__NITRON_RUNTIME__.routes["${routeName}"]`
682
+ );
683
+
684
+ const ext = args.path.split(".").pop();
685
+ const loader = ext === "tsx" ? "tsx" : ext === "ts" ? "ts" : ext === "jsx" ? "jsx" : "js";
686
+
687
+ return { contents: source, loader };
688
+ });
689
+ }
690
+ };
691
+ }
692
+
693
+ #createCssStubPlugin() {
694
+ return {
695
+ name: "css-stub",
696
+ setup: (build) => {
697
+ build.onResolve({ filter: /\.css$/ }, (args) => ({
698
+ path: args.path,
699
+ namespace: "css-stub"
700
+ }));
701
+
702
+ build.onLoad({ filter: /.*/, namespace: "css-stub" }, () => ({
703
+ contents: "",
704
+ loader: "js"
705
+ }));
706
+ }
707
+ };
708
+ }
709
+
710
+ #createMarkerPlugin(options) {
711
+ const isDev = this.#isDev;
712
+
713
+ return {
714
+ name: "client-marker",
715
+ setup: (build) => {
716
+ if (options.platform === "browser") {
717
+ return;
718
+ }
719
+
720
+ build.onLoad({ filter: /\.tsx$/ }, async (args) => {
721
+ const source = await fs.promises.readFile(args.path, "utf8");
722
+
723
+ if (!/^\s*["']use client["']/.test(source.slice(0, 50))) {
724
+ return null;
725
+ }
726
+
727
+ let ast;
728
+ try {
729
+ ast = parse(source, {
730
+ sourceType: "module",
731
+ plugins: ["typescript", "jsx"]
732
+ });
733
+ } catch {
734
+ if (isDev) {
735
+ console.warn(`${COLORS.yellow}⚠ Parse: ${args.path}${COLORS.reset}`);
736
+ }
737
+ return null;
738
+ }
739
+
740
+ const exports = this.#findExports(ast);
741
+ if (!exports.length) {
742
+ return null;
743
+ }
744
+
745
+ const symbolCode = `Symbol.for('__nitron_client_component__')`;
746
+ let additionalCode = "\n";
747
+
748
+ for (const exp of exports) {
749
+ additionalCode += `try { Object.defineProperty(${exp.name}, ${symbolCode}, { value: true }); ${exp.name}.displayName = "${exp.name}"; } catch {}\n`;
750
+ }
751
+
752
+ return { contents: source + additionalCode, loader: "tsx" };
753
+ });
754
+ }
755
+ };
756
+ }
757
+
758
+ async #processCss(filename, hasTailwind, processor) {
759
+ const inputPath = path.join(this.#paths.cssInput, filename);
760
+ const outputPath = path.join(this.#paths.cssOutput, filename);
761
+
762
+ await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
763
+
764
+ let content = this.#cache.css.get(inputPath);
765
+ if (!content) {
766
+ content = await fs.promises.readFile(inputPath, "utf8");
767
+ this.#cache.css.set(inputPath, content);
768
+ }
769
+
770
+ if (!hasTailwind) {
771
+ await fs.promises.writeFile(outputPath, content);
772
+ return;
773
+ }
774
+
775
+ const result = await processor.process(content, {
776
+ from: inputPath,
777
+ to: outputPath,
778
+ map: this.#isDev ? { inline: false } : false
779
+ });
780
+
781
+ await fs.promises.writeFile(outputPath, result.css);
782
+
783
+ if (result.map) {
784
+ await fs.promises.writeFile(`${outputPath}.map`, result.map.toString());
785
+ }
786
+ }
787
+
788
+ #detectTailwind() {
789
+ if (!fs.existsSync(this.#paths.cssInput)) {
790
+ return false;
791
+ }
792
+
793
+ const tailwindPattern = /@(import\s+["']tailwindcss["']|tailwind\s+(base|components|utilities))/;
794
+
795
+ for (const filename of fs.readdirSync(this.#paths.cssInput).filter(f => f.endsWith(".css"))) {
796
+ const filePath = path.join(this.#paths.cssInput, filename);
797
+
798
+ let content = this.#cache.css.get(filePath);
799
+ if (!content) {
800
+ content = fs.readFileSync(filePath, "utf8");
801
+ this.#cache.css.set(filePath, content);
802
+ }
803
+
804
+ if (tailwindPattern.test(content)) {
805
+ return true;
806
+ }
807
+ }
808
+
809
+ return false;
810
+ }
811
+
812
+ #discoverEntries(baseDir) {
813
+ if (!fs.existsSync(baseDir)) {
814
+ return { entries: [], meta: new Map() };
815
+ }
816
+
817
+ const files = this.#findTsxFiles(baseDir);
818
+ const { graph, imported, importedBy } = this.#buildDependencyGraph(files);
819
+
820
+ const entries = [...graph.entries()]
821
+ .filter(([file, meta]) => !imported.has(file) && meta.hasDefault && meta.jsx)
822
+ .map(([file]) => file);
823
+
824
+ this.#validateGraph(graph, entries, importedBy);
825
+
826
+ return { entries, meta: graph };
827
+ }
828
+
829
+ #findTsxFiles(dir, result = []) {
830
+ if (!fs.existsSync(dir)) {
831
+ return result;
832
+ }
833
+
834
+ for (const item of fs.readdirSync(dir)) {
835
+ const fullPath = path.join(dir, item);
836
+ const stat = fs.lstatSync(fullPath);
837
+
838
+ if (stat.isSymbolicLink()) {
839
+ continue;
840
+ }
841
+
842
+ if (stat.isDirectory()) {
843
+ this.#findTsxFiles(fullPath, result);
844
+ } else if (fullPath.endsWith(".tsx")) {
845
+ result.push(fullPath);
846
+ }
847
+ }
848
+
849
+ return result;
850
+ }
851
+
852
+ #buildDependencyGraph(files) {
853
+ const graph = new Map();
854
+ const imported = new Set();
855
+ const importedBy = new Map();
856
+
857
+ for (const file of files) {
858
+ const meta = this.#analyzeFile(file);
859
+ graph.set(file, meta);
860
+
861
+ for (const importPath of meta.imports) {
862
+ const resolvedPath = this.#resolveImport(file, importPath);
863
+
864
+ if (this.#isInRoot(resolvedPath)) {
865
+ imported.add(resolvedPath);
866
+
867
+ if (!importedBy.has(resolvedPath)) {
868
+ importedBy.set(resolvedPath, []);
869
+ }
870
+ importedBy.get(resolvedPath).push(file);
871
+ }
872
+ }
873
+ }
874
+
875
+ return { graph, imported, importedBy };
876
+ }
877
+
878
+ #analyzeFile(filePath) {
879
+ const source = fs.readFileSync(filePath, "utf8");
880
+
881
+ let ast;
882
+ try {
883
+ ast = parse(source, {
884
+ sourceType: "module",
885
+ plugins: ["typescript", "jsx"]
886
+ });
887
+ } catch {
888
+ return {
889
+ imports: [],
890
+ css: [],
891
+ hasDefault: false,
892
+ named: [],
893
+ jsx: false,
894
+ needsClient: false,
895
+ isClient: false
896
+ };
897
+ }
898
+
899
+ const isClient = ast.program.directives?.some(d => d.value?.value === "use client");
900
+
901
+ const meta = {
902
+ imports: new Set(),
903
+ css: new Set(),
904
+ hasDefault: false,
905
+ named: [],
906
+ jsx: false,
907
+ needsClient: false,
908
+ isClient,
909
+ reactNamespace: null
910
+ };
911
+
912
+ this.#traverse(ast, {
913
+ ImportDeclaration: (p) => {
914
+ const source = p.node.source.value;
915
+
916
+ if (source.startsWith(".")) {
917
+ meta.imports.add(source);
918
+ }
919
+
920
+ if (source.endsWith(".css")) {
921
+ const resolved = path.resolve(path.dirname(filePath), source);
922
+ if (resolved.startsWith(Paths.project)) {
923
+ meta.css.add(resolved);
924
+ }
925
+ }
926
+
927
+ if (source === "react") {
928
+ for (const specifier of p.node.specifiers) {
929
+ if (specifier.type === "ImportSpecifier" && CLIENT_HOOKS.has(specifier.imported.name)) {
930
+ meta.needsClient = true;
931
+ }
932
+ if (specifier.type === "ImportNamespaceSpecifier" || specifier.type === "ImportDefaultSpecifier") {
933
+ meta.reactNamespace = specifier.local.name;
934
+ }
935
+ }
936
+ }
937
+ },
938
+
939
+ MemberExpression: (p) => {
940
+ if (!meta.needsClient &&
941
+ p.node.object.name === meta.reactNamespace &&
942
+ CLIENT_HOOKS.has(p.node.property.name)) {
943
+ meta.needsClient = true;
944
+ }
945
+ },
946
+
947
+ CallExpression: (p) => {
948
+ if (!meta.needsClient &&
949
+ p.node.callee.type === "Identifier" &&
950
+ /^use[A-Z]/.test(p.node.callee.name)) {
951
+ meta.needsClient = true;
952
+ }
953
+ },
954
+
955
+ ExportNamedDeclaration: (p) => this.#extractNamedExports(p, meta),
956
+ ExportDefaultDeclaration: () => { meta.hasDefault = true; },
957
+ JSXElement: () => { meta.jsx = true; },
958
+ JSXFragment: () => { meta.jsx = true; }
959
+ });
960
+
961
+ if (meta.needsClient && !meta.isClient) {
962
+ throw this.#createError('Missing "use client"', {
963
+ File: path.relative(Paths.project, filePath),
964
+ Fix: 'Add "use client" at top'
965
+ });
966
+ }
967
+
968
+ return {
969
+ ...meta,
970
+ imports: [...meta.imports],
971
+ css: [...meta.css]
972
+ };
973
+ }
974
+
975
+ #extractNamedExports(path, meta) {
976
+ const declaration = path.node.declaration;
977
+
978
+ if (declaration?.type === "VariableDeclaration") {
979
+ for (const decl of declaration.declarations) {
980
+ if (decl.id.type === "Identifier" && decl.init?.type === "ArrowFunctionExpression") {
981
+ meta.named.push(decl.id.name);
982
+ }
983
+ }
984
+ }
985
+
986
+ if (declaration?.type === "FunctionDeclaration" && declaration.id?.name) {
987
+ meta.named.push(declaration.id.name);
988
+ }
989
+ }
990
+
991
+ #findExports(ast) {
992
+ const exports = [];
993
+
994
+ this.#traverse(ast, {
995
+ ExportDefaultDeclaration: (p) => {
996
+ const declaration = p.node.declaration;
997
+ let name;
998
+
999
+ if (declaration.type === "Identifier") {
1000
+ name = declaration.name;
1001
+ } else if (declaration.type === "FunctionDeclaration" && declaration.id?.name) {
1002
+ name = declaration.id.name;
1003
+ } else if (declaration.type === "CallExpression" &&
1004
+ ["memo", "forwardRef", "lazy"].includes(declaration.callee?.name)) {
1005
+ name = declaration.arguments[0]?.name || "__default__";
1006
+ } else {
1007
+ name = "__default__";
1008
+ }
1009
+
1010
+ exports.push({ name, isDefault: true });
1011
+ },
1012
+
1013
+ ExportNamedDeclaration: (p) => {
1014
+ for (const specifier of p.node.specifiers || []) {
1015
+ if (specifier.type === "ExportSpecifier") {
1016
+ const name = specifier.exported.name === "default"
1017
+ ? specifier.local.name
1018
+ : specifier.exported.name;
1019
+ exports.push({
1020
+ name,
1021
+ isDefault: specifier.exported.name === "default"
1022
+ });
1023
+ }
1024
+ }
1025
+
1026
+ const declaration = p.node.declaration;
1027
+
1028
+ if (declaration?.type === "FunctionDeclaration" && declaration.id?.name) {
1029
+ exports.push({ name: declaration.id.name, isDefault: false });
1030
+ }
1031
+
1032
+ if (declaration?.type === "VariableDeclaration") {
1033
+ for (const decl of declaration.declarations) {
1034
+ if (decl.id.type === "Identifier" && decl.init?.type === "ArrowFunctionExpression") {
1035
+ exports.push({ name: decl.id.name, isDefault: false });
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ });
1041
+
1042
+ return exports;
1043
+ }
1044
+
1045
+ #validateGraph(graph, entries, importedBy) {
1046
+ const entrySet = new Set(entries);
1047
+ const relativePath = (p) => path.relative(Paths.project, p);
1048
+
1049
+ for (const [filePath, meta] of graph.entries()) {
1050
+ if (!meta.isClient) {
1051
+ continue;
1052
+ }
1053
+
1054
+ if (entrySet.has(filePath) && meta.hasDefault && meta.jsx) {
1055
+ const importers = importedBy.get(filePath);
1056
+ if (importers?.length) {
1057
+ throw this.#createError("Client Entry Imported", {
1058
+ Entry: relativePath(filePath),
1059
+ By: relativePath(importers[0]),
1060
+ Fix: "Use as island"
1061
+ });
1062
+ }
1063
+ }
1064
+
1065
+ // Next.js pattern: Client component cannot import server component
1066
+ // Server component can render client component as children
1067
+ for (const importPath of meta.imports) {
1068
+ const resolvedPath = this.#resolveImport(filePath, importPath);
1069
+ const resolvedMeta = graph.get(resolvedPath);
1070
+
1071
+ // If importing a non-client component (server component)
1072
+ if (resolvedMeta && !resolvedMeta.isClient) {
1073
+ throw this.#createError("Boundary Violation", {
1074
+ Client: relativePath(filePath),
1075
+ Server: relativePath(resolvedPath),
1076
+ Fix: `Server components cannot be imported into client components. Use composition pattern: render "${path.basename(resolvedPath, ".tsx")}" as a parent and pass client component as children.`
1077
+ });
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ #addToManifest(entries, meta, baseDir, namespace) {
1084
+ for (const file of entries) {
1085
+ const fileMeta = meta.get(file);
1086
+ const viewPath = path.relative(baseDir, file)
1087
+ .replace(/\.tsx$/, "")
1088
+ .toLowerCase()
1089
+ .replace(/\\/g, "/");
1090
+
1091
+ const key = `${namespace}:${viewPath}`;
1092
+
1093
+ if (this.#manifest[key]) {
1094
+ throw new Error(`Duplicate: ${key}`);
1095
+ }
1096
+
1097
+ const cssFiles = [...this.#collectCss(file, meta, new Set())]
1098
+ .map(cssPath => `/css/${path.basename(cssPath)}`);
1099
+
1100
+ this.#manifest[key] = {
1101
+ css: cssFiles,
1102
+ hydrationScript: null
1103
+ };
1104
+ }
1105
+ }
1106
+
1107
+ #writeManifest() {
1108
+ const manifestPath = path.join(Paths.project, "build", "manifest.json");
1109
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
1110
+ fs.writeFileSync(manifestPath, JSON.stringify(this.#manifest, null, 2));
1111
+ }
1112
+
1113
+ #collectCss(file, meta, seen, depth = 0) {
1114
+ if (depth > MAX_DEPTH || seen.has(file)) {
1115
+ return new Set();
1116
+ }
1117
+ seen.add(file);
1118
+
1119
+ const fileMeta = meta.get(file);
1120
+ if (!fileMeta) {
1121
+ return new Set();
1122
+ }
1123
+
1124
+ const result = new Set(fileMeta.css);
1125
+
1126
+ for (const importPath of fileMeta.imports) {
1127
+ const resolvedPath = this.#resolveImport(file, importPath);
1128
+
1129
+ if (meta.has(resolvedPath)) {
1130
+ const childCss = this.#collectCss(resolvedPath, meta, seen, depth + 1);
1131
+ for (const cssPath of childCss) {
1132
+ result.add(cssPath);
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ return result;
1138
+ }
1139
+
1140
+ #resolveImport(fromFile, relativePath) {
1141
+ const cacheKey = `${fromFile}|${relativePath}`;
1142
+ const cached = this.#cache.imports.get(cacheKey);
1143
+
1144
+ if (cached) {
1145
+ return cached;
1146
+ }
1147
+
1148
+ const resolved = path.resolve(path.dirname(fromFile), relativePath);
1149
+
1150
+ if (!this.#isInRoot(resolved)) {
1151
+ throw this.#createError("Path Traversal", {
1152
+ Import: relativePath,
1153
+ Outside: resolved
1154
+ });
1155
+ }
1156
+
1157
+ const extensions = [".tsx", ".ts", ".jsx", ".js", "/index.tsx", "/index.ts", ""];
1158
+
1159
+ for (const ext of extensions) {
1160
+ const fullPath = resolved + ext;
1161
+
1162
+ if (ext === "" && fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
1163
+ this.#cache.imports.set(cacheKey, fullPath);
1164
+ return fullPath;
1165
+ }
1166
+
1167
+ if (ext && fs.existsSync(fullPath)) {
1168
+ this.#cache.imports.set(cacheKey, fullPath);
1169
+ return fullPath;
1170
+ }
1171
+ }
1172
+
1173
+ this.#cache.imports.set(cacheKey, resolved);
1174
+ return resolved;
1175
+ }
1176
+
1177
+ #isInRoot(filePath) {
1178
+ const normalized = path.normalize(filePath);
1179
+ const projectNormalized = path.normalize(Paths.project);
1180
+ const frameworkNormalized = path.normalize(Paths.framework);
1181
+
1182
+ // Allow both project paths and framework paths
1183
+ return normalized.startsWith(projectNormalized + path.sep) ||
1184
+ normalized === projectNormalized ||
1185
+ normalized.startsWith(frameworkNormalized + path.sep) ||
1186
+ normalized === frameworkNormalized;
1187
+ }
1188
+
1189
+ #writeJsxRuntime() {
1190
+ fs.mkdirSync(path.dirname(this.#paths.jsxRuntime), { recursive: true });
1191
+ fs.writeFileSync(this.#paths.jsxRuntime, JSX_RUNTIME);
1192
+ }
1193
+
1194
+ #cleanOutputDirs() {
1195
+ const dirsToClean = [
1196
+ this.#paths.cssOutput,
1197
+ this.#paths.jsOutput,
1198
+ this.#paths.nitronTemp
1199
+ ];
1200
+
1201
+ for (const dir of dirsToClean) {
1202
+ if (!dir.startsWith(Paths.project)) {
1203
+ throw new Error(`Unsafe path: ${dir}`);
1204
+ }
1205
+
1206
+ if (fs.existsSync(dir)) {
1207
+ fs.rmSync(dir, { recursive: true, force: true });
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ #cleanupTemp() {
1213
+ if (fs.existsSync(this.#paths.nitronTemp)) {
1214
+ if (!this.#paths.nitronTemp.startsWith(Paths.project)) {
1215
+ throw new Error(`Unsafe path: ${this.#paths.nitronTemp}`);
1216
+ }
1217
+ fs.rmSync(this.#paths.nitronTemp, { recursive: true, force: true });
1218
+ }
1219
+ }
1220
+
1221
+ #createError(title, details = {}) {
1222
+ let message = `\n${COLORS.red}✖ ${title}${COLORS.reset}\n\n`;
1223
+
1224
+ for (const [key, value] of Object.entries(details)) {
1225
+ message += ` ${key}: ${COLORS.yellow}${value}${COLORS.reset}\n`;
1226
+ }
1227
+
1228
+ return new Error(message);
1229
+ }
1230
+ }
1231
+
1232
+ export default Builder;