@salmansaeed/nexa 1.0.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.
- package/README.md +293 -0
- package/bin/nexa.js +1162 -0
- package/package.json +46 -0
- package/template/jsconfig.json +12 -0
- package/template/nexa.config.js +20 -0
- package/template/public/favicon.jpeg +0 -0
- package/template/public/index.html +14 -0
- package/template/public/logo.jpeg +0 -0
- package/template/public/manifest.json +20 -0
- package/template/src/App.css +117 -0
- package/template/src/App.jsx +44 -0
- package/template/src/components/DynamicHeader/DynamicHeader.css +55 -0
- package/template/src/components/DynamicHeader/DynamicHeader.jsx +39 -0
- package/template/src/components/Home/Home.css +189 -0
- package/template/src/components/Home/Home.jsx +24 -0
- package/template/src/components/Navbar/Navbar.css +217 -0
- package/template/src/components/Navbar/Navbar.jsx +85 -0
- package/template/src/components/Navbar/index.js +2 -0
- package/template/src/components/Nexa/Nexa.css +116 -0
- package/template/src/components/Nexa/Nexa.jsx +56 -0
- package/template/src/config/routeMeta.js +16 -0
- package/template/src/index.css +0 -0
- package/template/src/main.jsx +13 -0
package/bin/nexa.js
ADDED
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* File: bin/nexa.js
|
|
5
|
+
* Purpose: Main entrypoint for the Nexa CLI. This file parses commands and
|
|
6
|
+
* scaffolds apps, components, services, and contexts for React/Vite projects.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import os from "os";
|
|
12
|
+
import readline from "readline";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { execSync, spawn } from "child_process";
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Terminal colors for Nexa branding.
|
|
22
|
+
*/
|
|
23
|
+
const C = {
|
|
24
|
+
reset: "\x1b[0m",
|
|
25
|
+
cyan: "\x1b[36m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
blue: "\x1b[34m",
|
|
29
|
+
gray: "\x1b[90m",
|
|
30
|
+
bold: "\x1b[1m",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Purpose: Convert a string like "my-app" or "my_app" to PascalCase.
|
|
35
|
+
*/
|
|
36
|
+
function toPascalCase(str = "") {
|
|
37
|
+
return str
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/[\s-_]+/)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
42
|
+
.join("");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Purpose: Convert a string to kebab-case for folder/package names.
|
|
47
|
+
*/
|
|
48
|
+
function toKebabCase(str = "") {
|
|
49
|
+
return str
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
52
|
+
.split(/[\s_]+/)
|
|
53
|
+
.join("-")
|
|
54
|
+
.replace(/-+/g, "-")
|
|
55
|
+
.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Purpose: Convert a string into a safe CSS class prefix.
|
|
60
|
+
*/
|
|
61
|
+
function toCssClassName(str = "") {
|
|
62
|
+
return toKebabCase(str).replace(/[^a-z0-9-]/g, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Purpose: Ensure parent directories exist before writing a file.
|
|
67
|
+
*/
|
|
68
|
+
function writeFileSafe(filePath, content) {
|
|
69
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
70
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Purpose: Recursively copy directories and files, skipping junk folders.
|
|
75
|
+
*/
|
|
76
|
+
function copyRecursive(src, dest) {
|
|
77
|
+
if (!fs.existsSync(src)) return;
|
|
78
|
+
|
|
79
|
+
const stat = fs.statSync(src);
|
|
80
|
+
const base = path.basename(src);
|
|
81
|
+
|
|
82
|
+
const IGNORE_DIRS = ["node_modules", ".git"];
|
|
83
|
+
const IGNORE_EXACT_FILES = [".DS_Store"];
|
|
84
|
+
const IGNORE_FILE_PREFIXES = [".env"];
|
|
85
|
+
|
|
86
|
+
if (stat.isDirectory()) {
|
|
87
|
+
if (IGNORE_DIRS.includes(base)) return;
|
|
88
|
+
|
|
89
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
90
|
+
|
|
91
|
+
for (const item of fs.readdirSync(src)) {
|
|
92
|
+
copyRecursive(path.join(src, item), path.join(dest, item));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
if (
|
|
96
|
+
IGNORE_EXACT_FILES.includes(base) ||
|
|
97
|
+
IGNORE_FILE_PREFIXES.some(
|
|
98
|
+
(prefix) => base === prefix || base.startsWith(`${prefix}.`),
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
105
|
+
fs.copyFileSync(src, dest);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Purpose: Make sure generator commands are run from the project root,
|
|
111
|
+
* not from inside src or any nested folder.
|
|
112
|
+
*/
|
|
113
|
+
function ensureProjectRootForGenerators() {
|
|
114
|
+
const cwd = process.cwd();
|
|
115
|
+
const normalized = path.normalize(cwd);
|
|
116
|
+
const parts = normalized.split(path.sep).filter(Boolean);
|
|
117
|
+
|
|
118
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
119
|
+
const srcPath = path.join(cwd, "src");
|
|
120
|
+
|
|
121
|
+
const isInsideSrc =
|
|
122
|
+
parts[parts.length - 1] === "src" ||
|
|
123
|
+
normalized.includes(`${path.sep}src${path.sep}`);
|
|
124
|
+
|
|
125
|
+
if (isInsideSrc) {
|
|
126
|
+
console.error(
|
|
127
|
+
`${C.yellow}❌ Run Nexa generator commands from the project root, not from inside src.${C.reset}`,
|
|
128
|
+
);
|
|
129
|
+
console.error(
|
|
130
|
+
`${C.green}✅ Example: run 'nexa new gc MyComponent' from the app root.${C.reset}`,
|
|
131
|
+
);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!fs.existsSync(packageJsonPath) || !fs.existsSync(srcPath)) {
|
|
136
|
+
console.error(
|
|
137
|
+
`${C.yellow}❌ Nexa generator commands must be run from the project root folder.${C.reset}`,
|
|
138
|
+
);
|
|
139
|
+
console.error(
|
|
140
|
+
`${C.green}✅ Expected to find both package.json and src/ in the current directory.${C.reset}`,
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Purpose: Print usage help.
|
|
148
|
+
*/
|
|
149
|
+
function printUsage() {
|
|
150
|
+
console.log(`
|
|
151
|
+
${C.cyan}${C.bold}Nexa CLI Usage${C.reset}
|
|
152
|
+
${C.cyan}React Power.${C.reset}
|
|
153
|
+
${C.yellow}Angular Simplicity.${C.reset}
|
|
154
|
+
${C.green}Vite Speed.${C.reset}
|
|
155
|
+
${C.blue}Cleaner UI.${C.reset}
|
|
156
|
+
${C.gray}Prebuilt structure.${C.reset}
|
|
157
|
+
|
|
158
|
+
Run generator commands from the project root folder, not from src.
|
|
159
|
+
|
|
160
|
+
nexa new app <app-name>
|
|
161
|
+
nexa new gc <name>
|
|
162
|
+
nexa new cc <name>
|
|
163
|
+
nexa new svc <name>
|
|
164
|
+
nexa new ctx <name>
|
|
165
|
+
|
|
166
|
+
Convenience alias also supported:
|
|
167
|
+
nexa new <app-name>
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
nexa new app canna-core-420
|
|
171
|
+
nexa new canna-core-420
|
|
172
|
+
nexa new gc video-card
|
|
173
|
+
nexa new svc auth-service
|
|
174
|
+
nexa new ctx user-session
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Purpose: Ask the user a yes/no question in the terminal.
|
|
180
|
+
*/
|
|
181
|
+
function askYesNo(question) {
|
|
182
|
+
const rl = readline.createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
output: process.stdout,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
rl.question(question, (answer) => {
|
|
189
|
+
rl.close();
|
|
190
|
+
const normalized = answer.trim().toLowerCase();
|
|
191
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Purpose: Open the browser to the local dev server URL.
|
|
198
|
+
*/
|
|
199
|
+
function openBrowser(url) {
|
|
200
|
+
const platform = os.platform();
|
|
201
|
+
|
|
202
|
+
if (platform === "darwin") {
|
|
203
|
+
const child = spawn("open", [url], {
|
|
204
|
+
stdio: "ignore",
|
|
205
|
+
detached: true,
|
|
206
|
+
});
|
|
207
|
+
child.unref();
|
|
208
|
+
} else if (platform === "win32") {
|
|
209
|
+
const child = spawn("cmd", ["/c", "start", "", url], {
|
|
210
|
+
stdio: "ignore",
|
|
211
|
+
detached: true,
|
|
212
|
+
shell: true,
|
|
213
|
+
});
|
|
214
|
+
child.unref();
|
|
215
|
+
} else {
|
|
216
|
+
const child = spawn("xdg-open", [url], {
|
|
217
|
+
stdio: "ignore",
|
|
218
|
+
detached: true,
|
|
219
|
+
});
|
|
220
|
+
child.unref();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Purpose: Start the generated app in dev mode.
|
|
226
|
+
*/
|
|
227
|
+
function startGeneratedApp(projectDir, shouldOpenBrowser) {
|
|
228
|
+
const command = os.platform() === "win32" ? "npm.cmd" : "npm";
|
|
229
|
+
|
|
230
|
+
const devProcess = spawn(command, ["run", "dev"], {
|
|
231
|
+
cwd: projectDir,
|
|
232
|
+
stdio: "inherit",
|
|
233
|
+
shell: os.platform() === "win32",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (shouldOpenBrowser) {
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
openBrowser("http://localhost:4321");
|
|
239
|
+
}, 2500);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
devProcess.on("close", (code) => {
|
|
243
|
+
console.log(
|
|
244
|
+
`\n${C.green}✅ Dev server stopped (exit code ${code ?? 0})${C.reset}`,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Purpose: Create a service file in src/services.
|
|
251
|
+
*/
|
|
252
|
+
function createService(serviceName) {
|
|
253
|
+
ensureProjectRootForGenerators();
|
|
254
|
+
|
|
255
|
+
const finalName = toPascalCase(serviceName);
|
|
256
|
+
const servicePath = path.join(
|
|
257
|
+
process.cwd(),
|
|
258
|
+
"src",
|
|
259
|
+
"services",
|
|
260
|
+
`${finalName}.js`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const content = `/**
|
|
264
|
+
* File: src/services/${finalName}.js
|
|
265
|
+
* Purpose: Service module stub for ${finalName}.
|
|
266
|
+
*/
|
|
267
|
+
|
|
268
|
+
export default function ${finalName}() {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
writeFileSafe(servicePath, content);
|
|
274
|
+
console.log(
|
|
275
|
+
`${C.green}✅ Service created at src/services/${finalName}.js${C.reset}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Purpose: Create a context file in src/contexts.
|
|
281
|
+
*/
|
|
282
|
+
function createContext(contextName) {
|
|
283
|
+
ensureProjectRootForGenerators();
|
|
284
|
+
|
|
285
|
+
const finalName = toPascalCase(contextName);
|
|
286
|
+
const ctxPath = path.join(
|
|
287
|
+
process.cwd(),
|
|
288
|
+
"src",
|
|
289
|
+
"contexts",
|
|
290
|
+
`${finalName}.js`,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const content = `/**
|
|
294
|
+
* File: src/contexts/${finalName}.js
|
|
295
|
+
* Purpose: React context definition for ${finalName}.
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
import { createContext } from "react";
|
|
299
|
+
|
|
300
|
+
export const ${finalName} = createContext(null);
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
writeFileSafe(ctxPath, content);
|
|
304
|
+
console.log(
|
|
305
|
+
`${C.green}✅ Context created at src/contexts/${finalName}.js${C.reset}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Purpose: Create a component folder with JSX, child JSX, and CSS.
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
function createComponent(componentName) {
|
|
314
|
+
ensureProjectRootForGenerators();
|
|
315
|
+
|
|
316
|
+
const finalName = toPascalCase(componentName);
|
|
317
|
+
const childName = `${finalName}JS`;
|
|
318
|
+
const kebabName = toKebabCase(componentName);
|
|
319
|
+
const routePath = `/${kebabName}`;
|
|
320
|
+
|
|
321
|
+
const componentDir = path.join(process.cwd(), "src", "components", finalName);
|
|
322
|
+
const routeMetaPath = path.join(
|
|
323
|
+
process.cwd(),
|
|
324
|
+
"src",
|
|
325
|
+
"config",
|
|
326
|
+
"routeMeta.js",
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const jsxContent = `import React from "react";
|
|
330
|
+
import "./${finalName}.css";
|
|
331
|
+
import ${childName} from "./${childName}";
|
|
332
|
+
|
|
333
|
+
const ${finalName} = () => {
|
|
334
|
+
return (
|
|
335
|
+
<section className="home-page">
|
|
336
|
+
<div className="nexa-intro">
|
|
337
|
+
<div className="nexa-logo-wrap">
|
|
338
|
+
<div className="nexa-logo-convex" />
|
|
339
|
+
<div className="nexa-logo-shine" />
|
|
340
|
+
<h1 className="nexa-logo">N</h1>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<h2 className="nexa-wordmark">${finalName}</h2>
|
|
344
|
+
<p className="nexa-tagline">Cleaner UI. Prebuilt structure.</p>
|
|
345
|
+
<p className="nexa-credit">
|
|
346
|
+
A product of <span>Conscious Neurons</span>
|
|
347
|
+
</p>
|
|
348
|
+
|
|
349
|
+
<p className="${kebabName}-note">
|
|
350
|
+
This component was created by Nexa.
|
|
351
|
+
</p>
|
|
352
|
+
|
|
353
|
+
<div className="${kebabName}-child">
|
|
354
|
+
<${childName} />
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</section>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default ${finalName};
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
const childContent = `import React from "react";
|
|
365
|
+
|
|
366
|
+
const ${childName} = () => {
|
|
367
|
+
return (
|
|
368
|
+
<div className="${kebabName}-child-inner">
|
|
369
|
+
<p>This is the ${childName} child component.</p>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export default ${childName};
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const cssContent = `.home-page {
|
|
378
|
+
min-height: calc(100vh - 120px);
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
padding: 24px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.nexa-intro {
|
|
386
|
+
text-align: center;
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: column;
|
|
389
|
+
align-items: center;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.nexa-logo-wrap {
|
|
394
|
+
position: relative;
|
|
395
|
+
width: 190px;
|
|
396
|
+
height: 190px;
|
|
397
|
+
display: grid;
|
|
398
|
+
place-items: center;
|
|
399
|
+
margin-bottom: 20px;
|
|
400
|
+
border-radius: 40px;
|
|
401
|
+
background: radial-gradient(
|
|
402
|
+
circle at center,
|
|
403
|
+
rgba(62, 231, 255, 0.1),
|
|
404
|
+
rgba(255, 255, 255, 0.02)
|
|
405
|
+
);
|
|
406
|
+
box-shadow:
|
|
407
|
+
0 0 60px rgba(62, 231, 255, 0.1),
|
|
408
|
+
inset 0 0 30px rgba(255, 255, 255, 0.025);
|
|
409
|
+
overflow: hidden;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.nexa-logo-convex {
|
|
413
|
+
position: absolute;
|
|
414
|
+
inset: 10%;
|
|
415
|
+
border-radius: 28px;
|
|
416
|
+
background: radial-gradient(
|
|
417
|
+
circle at 35% 28%,
|
|
418
|
+
rgba(255, 255, 255, 0.16),
|
|
419
|
+
rgba(255, 255, 255, 0.04) 28%,
|
|
420
|
+
rgba(255, 255, 255, 0.01) 52%,
|
|
421
|
+
rgba(0, 0, 0, 0.04) 100%
|
|
422
|
+
);
|
|
423
|
+
box-shadow:
|
|
424
|
+
inset 0 2px 10px rgba(255, 255, 255, 0.06),
|
|
425
|
+
inset 0 -10px 18px rgba(0, 0, 0, 0.12);
|
|
426
|
+
pointer-events: none;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.nexa-logo {
|
|
430
|
+
margin: 0;
|
|
431
|
+
font-size: 7rem;
|
|
432
|
+
line-height: 1;
|
|
433
|
+
font-weight: 800;
|
|
434
|
+
letter-spacing: -0.08em;
|
|
435
|
+
color: var(--nexa-primary);
|
|
436
|
+
text-shadow:
|
|
437
|
+
0 0 10px rgba(62, 231, 255, 0.28),
|
|
438
|
+
0 0 24px rgba(62, 231, 255, 0.14);
|
|
439
|
+
transform: scale(0.2);
|
|
440
|
+
opacity: 0;
|
|
441
|
+
filter: blur(14px);
|
|
442
|
+
animation: nexaReveal 1.2s ease-out forwards;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.nexa-logo-shine {
|
|
446
|
+
position: absolute;
|
|
447
|
+
inset: -40%;
|
|
448
|
+
background: linear-gradient(
|
|
449
|
+
110deg,
|
|
450
|
+
transparent 35%,
|
|
451
|
+
rgba(255, 255, 255, 0.04) 45%,
|
|
452
|
+
rgba(255, 255, 255, 0.35) 50%,
|
|
453
|
+
rgba(255, 255, 255, 0.04) 55%,
|
|
454
|
+
transparent 65%
|
|
455
|
+
);
|
|
456
|
+
transform: translateX(-140%) rotate(12deg);
|
|
457
|
+
animation: nexaShine 1.6s ease 0.65s forwards;
|
|
458
|
+
pointer-events: none;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.nexa-wordmark {
|
|
462
|
+
margin: 0 0 8px;
|
|
463
|
+
font-size: clamp(2.2rem, 5vw, 3.8rem);
|
|
464
|
+
font-weight: 700;
|
|
465
|
+
letter-spacing: -0.04em;
|
|
466
|
+
color: var(--nexa-text);
|
|
467
|
+
opacity: 0;
|
|
468
|
+
transform: translateY(10px);
|
|
469
|
+
animation: fadeUp 0.7s ease 0.8s forwards;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.nexa-tagline {
|
|
473
|
+
margin: 0;
|
|
474
|
+
font-size: 1rem;
|
|
475
|
+
color: var(--nexa-text-dim);
|
|
476
|
+
letter-spacing: 0.02em;
|
|
477
|
+
opacity: 0;
|
|
478
|
+
transform: translateY(10px);
|
|
479
|
+
animation: fadeUp 0.7s ease 1s forwards;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.nexa-credit {
|
|
483
|
+
margin-top: 10px;
|
|
484
|
+
font-size: 0.82rem;
|
|
485
|
+
color: var(--nexa-text-dim);
|
|
486
|
+
opacity: 0;
|
|
487
|
+
transform: translateY(10px);
|
|
488
|
+
animation: fadeUp 0.7s ease 1.2s forwards;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.nexa-credit span {
|
|
492
|
+
color: var(--nexa-accent);
|
|
493
|
+
font-weight: 600;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.${kebabName}-note {
|
|
497
|
+
margin-top: 10px;
|
|
498
|
+
font-size: 0.9rem;
|
|
499
|
+
color: var(--nexa-text-dim);
|
|
500
|
+
opacity: 0;
|
|
501
|
+
transform: translateY(10px);
|
|
502
|
+
animation: fadeUp 0.7s ease 1.35s forwards;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.${kebabName}-child {
|
|
506
|
+
margin-top: 14px;
|
|
507
|
+
opacity: 0;
|
|
508
|
+
transform: translateY(10px);
|
|
509
|
+
animation: fadeUp 0.7s ease 1.5s forwards;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.${kebabName}-child-inner {
|
|
513
|
+
padding: 0;
|
|
514
|
+
margin: 0;
|
|
515
|
+
background: transparent;
|
|
516
|
+
border: none;
|
|
517
|
+
box-shadow: none;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.${kebabName}-child-inner p {
|
|
521
|
+
margin: 0;
|
|
522
|
+
color: var(--nexa-text-dim);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
@keyframes nexaReveal {
|
|
526
|
+
0% {
|
|
527
|
+
transform: scale(0.2);
|
|
528
|
+
opacity: 0;
|
|
529
|
+
filter: blur(14px);
|
|
530
|
+
}
|
|
531
|
+
55% {
|
|
532
|
+
transform: scale(1.08);
|
|
533
|
+
opacity: 1;
|
|
534
|
+
filter: blur(0);
|
|
535
|
+
}
|
|
536
|
+
100% {
|
|
537
|
+
transform: scale(1);
|
|
538
|
+
opacity: 1;
|
|
539
|
+
filter: blur(0);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
@keyframes nexaShine {
|
|
544
|
+
0% {
|
|
545
|
+
transform: translateX(-140%) rotate(12deg);
|
|
546
|
+
opacity: 0;
|
|
547
|
+
}
|
|
548
|
+
20% {
|
|
549
|
+
opacity: 1;
|
|
550
|
+
}
|
|
551
|
+
100% {
|
|
552
|
+
transform: translateX(140%) rotate(12deg);
|
|
553
|
+
opacity: 0;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
@keyframes fadeUp {
|
|
558
|
+
to {
|
|
559
|
+
opacity: 1;
|
|
560
|
+
transform: translateY(0);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
@media (max-width: 768px) {
|
|
565
|
+
.home-page {
|
|
566
|
+
min-height: calc(100vh - 100px);
|
|
567
|
+
padding: 16px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.nexa-logo-wrap {
|
|
571
|
+
width: 160px;
|
|
572
|
+
height: 160px;
|
|
573
|
+
border-radius: 32px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.nexa-logo-convex {
|
|
577
|
+
border-radius: 22px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.nexa-logo {
|
|
581
|
+
font-size: 5.8rem;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.nexa-wordmark {
|
|
585
|
+
font-size: 2.3rem;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.nexa-tagline {
|
|
589
|
+
font-size: 0.92rem;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
`;
|
|
593
|
+
|
|
594
|
+
writeFileSafe(path.join(componentDir, `${finalName}.jsx`), jsxContent);
|
|
595
|
+
writeFileSafe(path.join(componentDir, `${childName}.jsx`), childContent);
|
|
596
|
+
writeFileSafe(path.join(componentDir, `${finalName}.css`), cssContent);
|
|
597
|
+
|
|
598
|
+
if (fs.existsSync(routeMetaPath)) {
|
|
599
|
+
let routeMetaContent = fs.readFileSync(routeMetaPath, "utf8");
|
|
600
|
+
|
|
601
|
+
if (!routeMetaContent.includes(`"${routePath}"`)) {
|
|
602
|
+
const entry = ` "${routePath}": {
|
|
603
|
+
navLabel: "${finalName}",
|
|
604
|
+
title: "${finalName}",
|
|
605
|
+
subtitle: "Generated instantly by Nexa CLI",
|
|
606
|
+
tooltip: "This page was auto-generated by the Nexa CLI",
|
|
607
|
+
showInNav: true,
|
|
608
|
+
}`;
|
|
609
|
+
|
|
610
|
+
routeMetaContent = routeMetaContent.replace(
|
|
611
|
+
/(\s*)};\s*$/,
|
|
612
|
+
`,\n${entry}\n};`,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
routeMetaContent = routeMetaContent.replace(/,\s*,/g, ",");
|
|
616
|
+
|
|
617
|
+
fs.writeFileSync(routeMetaPath, routeMetaContent, "utf8");
|
|
618
|
+
console.log(
|
|
619
|
+
`${C.blue}ℹ Added routeMeta entry for ${routePath}${C.reset}`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
console.log(
|
|
624
|
+
`${C.yellow}⚠ routeMeta.js not found. Component created, but route was not added.${C.reset}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const importPath = `./components/${finalName}/${finalName}`;
|
|
629
|
+
|
|
630
|
+
console.log(`
|
|
631
|
+
${C.cyan}📌 Next Step:${C.reset}
|
|
632
|
+
|
|
633
|
+
${C.yellow}1. Import your component in App.jsx:${C.reset}
|
|
634
|
+
${C.gray}import ${finalName} from "${importPath}";${C.reset}
|
|
635
|
+
|
|
636
|
+
${C.yellow}2. Add the route inside <Routes>:${C.reset}
|
|
637
|
+
${C.gray}<Route path="${routePath}" element={<${finalName} />} />${C.reset}
|
|
638
|
+
`);
|
|
639
|
+
|
|
640
|
+
console.log(
|
|
641
|
+
`${C.green}✅ Component '${finalName}' created at src/components/${finalName}${C.reset}`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Purpose: Create a full app scaffold from the template folder and then
|
|
646
|
+
* patch key files with the correct app-specific values.
|
|
647
|
+
*/
|
|
648
|
+
async function createApp(rawAppName) {
|
|
649
|
+
const projectDirName = toKebabCase(rawAppName);
|
|
650
|
+
const displayName = toPascalCase(rawAppName);
|
|
651
|
+
const packageName = toKebabCase(rawAppName);
|
|
652
|
+
|
|
653
|
+
const root = process.cwd();
|
|
654
|
+
const projectDir = path.join(root, projectDirName);
|
|
655
|
+
const templateDir = path.join(__dirname, "../template");
|
|
656
|
+
|
|
657
|
+
if (!rawAppName) {
|
|
658
|
+
console.error(`${C.yellow}❌ Please provide an app name.${C.reset}`);
|
|
659
|
+
console.error(`${C.green}Example: nexa new app canna-core-420${C.reset}`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!fs.existsSync(templateDir)) {
|
|
664
|
+
console.error(`${C.yellow}❌ Template directory not found.${C.reset}`);
|
|
665
|
+
console.error(`${C.gray}Expected template at: ${templateDir}${C.reset}`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (fs.existsSync(projectDir)) {
|
|
670
|
+
console.error(
|
|
671
|
+
`${C.yellow}❌ Folder already exists: ${projectDirName}${C.reset}`,
|
|
672
|
+
);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
677
|
+
|
|
678
|
+
copyRecursive(templateDir, projectDir);
|
|
679
|
+
|
|
680
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
681
|
+
|
|
682
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
683
|
+
const gitignoreContent = `# Dependencies
|
|
684
|
+
node_modules/
|
|
685
|
+
npm-debug.log*
|
|
686
|
+
yarn-debug.log*
|
|
687
|
+
yarn-error.log*
|
|
688
|
+
|
|
689
|
+
# Build output
|
|
690
|
+
dist/
|
|
691
|
+
build/
|
|
692
|
+
|
|
693
|
+
# Environment variables
|
|
694
|
+
.env
|
|
695
|
+
.env.*
|
|
696
|
+
!.env.example
|
|
697
|
+
|
|
698
|
+
# Logs
|
|
699
|
+
logs/
|
|
700
|
+
*.log
|
|
701
|
+
|
|
702
|
+
# OS files
|
|
703
|
+
.DS_Store
|
|
704
|
+
Thumbs.db
|
|
705
|
+
|
|
706
|
+
# Editor / IDE
|
|
707
|
+
.vscode/
|
|
708
|
+
.idea/
|
|
709
|
+
*.suo
|
|
710
|
+
*.ntvs*
|
|
711
|
+
*.njsproj
|
|
712
|
+
*.sln
|
|
713
|
+
|
|
714
|
+
# Temporary files
|
|
715
|
+
tmp/
|
|
716
|
+
temp/
|
|
717
|
+
*.tmp
|
|
718
|
+
|
|
719
|
+
# Coverage
|
|
720
|
+
coverage/
|
|
721
|
+
|
|
722
|
+
# Cache
|
|
723
|
+
.cache/
|
|
724
|
+
.parcel-cache/
|
|
725
|
+
.vite/
|
|
726
|
+
|
|
727
|
+
# Optional: lock files (keep if you want reproducible builds)
|
|
728
|
+
# package-lock.json
|
|
729
|
+
# yarn.lock
|
|
730
|
+
|
|
731
|
+
# Misc
|
|
732
|
+
*.tgz
|
|
733
|
+
`;
|
|
734
|
+
writeFileSafe(gitignorePath, gitignoreContent);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const publicDir = path.join(projectDir, "public");
|
|
738
|
+
const srcDir = path.join(projectDir, "src");
|
|
739
|
+
|
|
740
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
741
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
742
|
+
|
|
743
|
+
const publicIndexPath = path.join(publicDir, "index.html");
|
|
744
|
+
const rootIndexPath = path.join(projectDir, "index.html");
|
|
745
|
+
|
|
746
|
+
if (fs.existsSync(publicIndexPath)) {
|
|
747
|
+
const templateIndex = fs.readFileSync(publicIndexPath, "utf8");
|
|
748
|
+
const patchedIndex = templateIndex
|
|
749
|
+
.replace(/<title>.*?<\/title>/i, `<title>${displayName}</title>`)
|
|
750
|
+
.replace(/src=["']\.\/src\/main\.jsx["']/i, 'src="/src/main.jsx"')
|
|
751
|
+
.replace(/src=["']src\/main\.jsx["']/i, 'src="/src/main.jsx"');
|
|
752
|
+
|
|
753
|
+
writeFileSafe(rootIndexPath, patchedIndex);
|
|
754
|
+
fs.unlinkSync(publicIndexPath);
|
|
755
|
+
} else if (!fs.existsSync(rootIndexPath)) {
|
|
756
|
+
const indexHtmlContent = `<!DOCTYPE html>
|
|
757
|
+
<html lang="en">
|
|
758
|
+
<head>
|
|
759
|
+
<!-- File: index.html -->
|
|
760
|
+
<!-- Purpose: Root HTML document for the generated Vite application. -->
|
|
761
|
+
<meta charset="UTF-8" />
|
|
762
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
763
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
764
|
+
<link rel="manifest" href="/manifest.json" />
|
|
765
|
+
<title>${displayName}</title>
|
|
766
|
+
</head>
|
|
767
|
+
<body>
|
|
768
|
+
<div id="root"></div>
|
|
769
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
770
|
+
</body>
|
|
771
|
+
</html>
|
|
772
|
+
`;
|
|
773
|
+
writeFileSafe(rootIndexPath, indexHtmlContent);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const manifestPath = path.join(publicDir, "manifest.json");
|
|
777
|
+
const manifestContent = `{
|
|
778
|
+
"name": "${displayName}",
|
|
779
|
+
"short_name": "${displayName}",
|
|
780
|
+
"start_url": ".",
|
|
781
|
+
"display": "standalone",
|
|
782
|
+
"background_color": "#0a0f24",
|
|
783
|
+
"theme_color": "#ffd700"
|
|
784
|
+
}
|
|
785
|
+
`;
|
|
786
|
+
writeFileSafe(manifestPath, manifestContent);
|
|
787
|
+
|
|
788
|
+
const generatedPkg = {
|
|
789
|
+
name: packageName,
|
|
790
|
+
version: "1.0.0",
|
|
791
|
+
private: true,
|
|
792
|
+
type: "module",
|
|
793
|
+
scripts: {
|
|
794
|
+
dev: "node run.js",
|
|
795
|
+
start: "node run.js",
|
|
796
|
+
nexa: "node run.js",
|
|
797
|
+
build: "vite build",
|
|
798
|
+
preview: "vite preview",
|
|
799
|
+
},
|
|
800
|
+
dependencies: {
|
|
801
|
+
react: "^18.3.1",
|
|
802
|
+
"react-dom": "^18.3.1",
|
|
803
|
+
"react-router-dom": "^6.15.0",
|
|
804
|
+
},
|
|
805
|
+
devDependencies: {
|
|
806
|
+
vite: "^7.2.7",
|
|
807
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
writeFileSafe(
|
|
812
|
+
path.join(projectDir, "package.json"),
|
|
813
|
+
`${JSON.stringify(generatedPkg, null, 2)}\n`,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const mainJsxPath = path.join(srcDir, "main.jsx");
|
|
817
|
+
if (!fs.existsSync(mainJsxPath)) {
|
|
818
|
+
const mainJsxContent = `/**
|
|
819
|
+
* File: src/main.jsx
|
|
820
|
+
* Purpose: Entry point for the React application. Mounts App into the root DOM node.
|
|
821
|
+
*/
|
|
822
|
+
|
|
823
|
+
import React from "react";
|
|
824
|
+
import ReactDOM from "react-dom/client";
|
|
825
|
+
import App from "./App.jsx";
|
|
826
|
+
import "./index.css";
|
|
827
|
+
|
|
828
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
829
|
+
<React.StrictMode>
|
|
830
|
+
<App />
|
|
831
|
+
</React.StrictMode>
|
|
832
|
+
);
|
|
833
|
+
`;
|
|
834
|
+
writeFileSafe(mainJsxPath, mainJsxContent);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const appJsxPath = path.join(srcDir, "App.jsx");
|
|
838
|
+
if (!fs.existsSync(appJsxPath)) {
|
|
839
|
+
const appJsxContent = `/**
|
|
840
|
+
* File: src/App.jsx
|
|
841
|
+
* Purpose: Main root component for the generated Nexa application.
|
|
842
|
+
*/
|
|
843
|
+
|
|
844
|
+
import React from "react";
|
|
845
|
+
import "./App.css";
|
|
846
|
+
|
|
847
|
+
const App = () => {
|
|
848
|
+
return (
|
|
849
|
+
<div className="app-container">
|
|
850
|
+
<h1>Welcome to ${displayName}</h1>
|
|
851
|
+
<p>Your Nexa app is ready.</p>
|
|
852
|
+
<p>React Power. Angular Simplicity. Vite Speed.</p>
|
|
853
|
+
<p>Cleaner UI. Prebuilt structure.</p>
|
|
854
|
+
<p>
|
|
855
|
+
Powered by{" "}
|
|
856
|
+
<a
|
|
857
|
+
href="https://consciousneurons.com"
|
|
858
|
+
target="_blank"
|
|
859
|
+
rel="noopener noreferrer"
|
|
860
|
+
>
|
|
861
|
+
Conscious Neurons LLC
|
|
862
|
+
</a>
|
|
863
|
+
{" "} | Sponsored by{" "}
|
|
864
|
+
<a
|
|
865
|
+
href="https://albagoldsystems.com"
|
|
866
|
+
target="_blank"
|
|
867
|
+
rel="noopener noreferrer"
|
|
868
|
+
>
|
|
869
|
+
Alba Gold
|
|
870
|
+
</a>
|
|
871
|
+
</p>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
export default App;
|
|
877
|
+
`;
|
|
878
|
+
writeFileSafe(appJsxPath, appJsxContent);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const appCssPath = path.join(srcDir, "App.css");
|
|
882
|
+
if (!fs.existsSync(appCssPath)) {
|
|
883
|
+
const appCssContent = `/**
|
|
884
|
+
* File: src/App.css
|
|
885
|
+
* Purpose: Base styling for the generated Nexa application shell.
|
|
886
|
+
*/
|
|
887
|
+
|
|
888
|
+
body {
|
|
889
|
+
margin: 0;
|
|
890
|
+
font-family: Inter, sans-serif;
|
|
891
|
+
background-color: #0a0f24;
|
|
892
|
+
color: #f8f9fc;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
a {
|
|
896
|
+
color: #ffd700;
|
|
897
|
+
text-decoration: none;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
a:hover {
|
|
901
|
+
text-decoration: underline;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.app-container {
|
|
905
|
+
display: flex;
|
|
906
|
+
min-height: 100vh;
|
|
907
|
+
padding: 32px;
|
|
908
|
+
flex-direction: column;
|
|
909
|
+
align-items: center;
|
|
910
|
+
justify-content: center;
|
|
911
|
+
text-align: center;
|
|
912
|
+
}
|
|
913
|
+
`;
|
|
914
|
+
writeFileSafe(appCssPath, appCssContent);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const runJsPath = path.join(projectDir, "run.js");
|
|
918
|
+
if (!fs.existsSync(runJsPath)) {
|
|
919
|
+
const runJsContent = `#!/usr/bin/env node
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* File: run.js
|
|
923
|
+
* Purpose: Launches the Nexa app using Vite with custom config.
|
|
924
|
+
*/
|
|
925
|
+
|
|
926
|
+
import { spawn } from "child_process";
|
|
927
|
+
import path from "path";
|
|
928
|
+
import os from "os";
|
|
929
|
+
import fs from "fs";
|
|
930
|
+
|
|
931
|
+
const C = {
|
|
932
|
+
reset: "\\x1b[0m",
|
|
933
|
+
cyan: "\\x1b[36m",
|
|
934
|
+
yellow: "\\x1b[33m",
|
|
935
|
+
green: "\\x1b[32m",
|
|
936
|
+
blue: "\\x1b[34m",
|
|
937
|
+
gray: "\\x1b[90m",
|
|
938
|
+
bold: "\\x1b[1m",
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
console.log(\`
|
|
942
|
+
\${C.cyan}\${C.bold}🚀 Nexa CLI\${C.reset} - Powered by Conscious Neurons LLC
|
|
943
|
+
\${C.gray}https://consciousneurons.com\${C.reset}
|
|
944
|
+
Built by Salman Saeed
|
|
945
|
+
|
|
946
|
+
\${C.yellow}Cleaner UI. Prebuilt structure.\${C.reset}
|
|
947
|
+
\${C.gray}Everything important is already in place.\${C.reset}
|
|
948
|
+
|
|
949
|
+
\${C.green}🔹 Starting your Nexa App...\${C.reset}
|
|
950
|
+
\${C.cyan}🔹 React Power.\${C.reset}
|
|
951
|
+
\${C.yellow}🔹 Angular Simplicity.\${C.reset}
|
|
952
|
+
\${C.blue}🔹 Vite Speed.\${C.reset}
|
|
953
|
+
\${C.green}🔹 Cleaner UI.\${C.reset}
|
|
954
|
+
\${C.gray}🔹 Prebuilt structure.${C.reset}
|
|
955
|
+
|
|
956
|
+
\${C.cyan}███╗ ██╗███████╗██╗ ██╗ █████╗\${C.reset}
|
|
957
|
+
\${C.cyan}████╗ ██║██╔════╝╚██╗██╔╝██╔══██╗\${C.reset}
|
|
958
|
+
\${C.cyan}██╔██╗ ██║█████╗ ╚███╔╝ ███████║\${C.reset}
|
|
959
|
+
\${C.cyan}██║╚██╗██║██╔══╝ ██╔██╗ ██╔══██║\${C.reset}
|
|
960
|
+
\${C.cyan}██║ ╚████║███████╗██╔╝ ██╗██║ ██║\${C.reset}
|
|
961
|
+
\${C.cyan}╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝\${C.reset}
|
|
962
|
+
|
|
963
|
+
\${C.gray}by https://salmansaeed.us\${C.reset}
|
|
964
|
+
\`);
|
|
965
|
+
|
|
966
|
+
const configPath = path.resolve("./nexa.config.js");
|
|
967
|
+
const hasConfig = fs.existsSync(configPath);
|
|
968
|
+
|
|
969
|
+
const command = os.platform() === "win32" ? "npx.cmd" : "npx";
|
|
970
|
+
const viteArgs = ["vite"];
|
|
971
|
+
|
|
972
|
+
if (hasConfig) {
|
|
973
|
+
viteArgs.push("--config", configPath);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const vite = spawn(command, viteArgs, {
|
|
977
|
+
stdio: "pipe",
|
|
978
|
+
shell: os.platform() === "win32",
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
vite.stdout.on("data", (data) => {
|
|
982
|
+
const str = data.toString();
|
|
983
|
+
if (!str.includes("VITE")) {
|
|
984
|
+
console.log(str);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
vite.stderr.on("data", (data) => {
|
|
989
|
+
process.stderr.write(data);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
vite.on("close", (code) => {
|
|
993
|
+
console.log(\`\\n\${C.green}✅ Nexa App stopped (exit code \${code ?? 0})\${C.reset}\`);
|
|
994
|
+
process.exit(code ?? 0);
|
|
995
|
+
});
|
|
996
|
+
`;
|
|
997
|
+
writeFileSafe(runJsPath, runJsContent);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const nexaConfigPath = path.join(projectDir, "nexa.config.js");
|
|
1001
|
+
if (!fs.existsSync(nexaConfigPath)) {
|
|
1002
|
+
const nexaConfigContent = `/**
|
|
1003
|
+
* File: nexa.config.js
|
|
1004
|
+
* Purpose: Vite configuration for Nexa-generated apps.
|
|
1005
|
+
*/
|
|
1006
|
+
|
|
1007
|
+
import { defineConfig } from "vite";
|
|
1008
|
+
import react from "@vitejs/plugin-react";
|
|
1009
|
+
|
|
1010
|
+
export default defineConfig({
|
|
1011
|
+
root: ".",
|
|
1012
|
+
base: "/",
|
|
1013
|
+
plugins: [react()],
|
|
1014
|
+
server: {
|
|
1015
|
+
port: 4321,
|
|
1016
|
+
},
|
|
1017
|
+
build: {
|
|
1018
|
+
outDir: "dist",
|
|
1019
|
+
},
|
|
1020
|
+
clearScreen: false,
|
|
1021
|
+
});
|
|
1022
|
+
`;
|
|
1023
|
+
writeFileSafe(nexaConfigPath, nexaConfigContent);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
console.log(`\n${C.blue}📦 Installing dependencies...${C.reset}`);
|
|
1027
|
+
let installSucceeded = false;
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
execSync("npm install", { stdio: "inherit", cwd: projectDir });
|
|
1031
|
+
installSucceeded = true;
|
|
1032
|
+
console.log(`${C.green}✅ Dependencies installed successfully!${C.reset}`);
|
|
1033
|
+
} catch {
|
|
1034
|
+
console.error(
|
|
1035
|
+
`${C.yellow}❌ Failed to install dependencies. Run 'npm install' manually.${C.reset}`,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
console.log(`\n${C.green}🎉 Project created successfully!${C.reset}`);
|
|
1040
|
+
console.log(`${C.cyan}React Power.${C.reset}`);
|
|
1041
|
+
console.log(`${C.yellow}Angular Simplicity.${C.reset}`);
|
|
1042
|
+
console.log(`${C.green}Vite Speed.${C.reset}`);
|
|
1043
|
+
console.log(`${C.blue}Cleaner UI.${C.reset}`);
|
|
1044
|
+
console.log(`${C.gray}Prebuilt structure.${C.reset}`);
|
|
1045
|
+
console.log(`${C.gray}cd ${projectDirName}${C.reset}`);
|
|
1046
|
+
console.log(`${C.gray}npm run dev${C.reset}`);
|
|
1047
|
+
console.log(`${C.gray}npm run build${C.reset}`);
|
|
1048
|
+
console.log(`${C.gray}npm run preview${C.reset}`);
|
|
1049
|
+
|
|
1050
|
+
if (installSucceeded) {
|
|
1051
|
+
const shouldStart = await askYesNo(
|
|
1052
|
+
`\n${C.green}🚀 Auto start the app now? (y/n): ${C.reset}`,
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
if (shouldStart) {
|
|
1056
|
+
const shouldOpen = await askYesNo(
|
|
1057
|
+
`${C.blue}🌐 Open in browser automatically? (y/n): ${C.reset}`,
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
console.log(
|
|
1061
|
+
`\n${C.green}▶️ Starting app in ${projectDirName}...${C.reset}\n`,
|
|
1062
|
+
);
|
|
1063
|
+
startGeneratedApp(projectDir, shouldOpen);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Purpose: Parse command arguments and support:
|
|
1070
|
+
* nexa new app my-app
|
|
1071
|
+
* nexa new my-app
|
|
1072
|
+
* nexa app my-app
|
|
1073
|
+
*/
|
|
1074
|
+
function parseArgs(argv) {
|
|
1075
|
+
const first = argv[0];
|
|
1076
|
+
const second = argv[1];
|
|
1077
|
+
const third = argv[2];
|
|
1078
|
+
|
|
1079
|
+
if (!first) {
|
|
1080
|
+
printUsage();
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (first === "new" && ["app", "gc", "cc", "svc", "ctx"].includes(second)) {
|
|
1085
|
+
return {
|
|
1086
|
+
shortcut: second,
|
|
1087
|
+
name: third,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (
|
|
1092
|
+
first === "new" &&
|
|
1093
|
+
second &&
|
|
1094
|
+
!["app", "gc", "cc", "svc", "ctx"].includes(second)
|
|
1095
|
+
) {
|
|
1096
|
+
return {
|
|
1097
|
+
shortcut: "app",
|
|
1098
|
+
name: second,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (["app", "gc", "cc", "svc", "ctx"].includes(first)) {
|
|
1103
|
+
return {
|
|
1104
|
+
shortcut: first,
|
|
1105
|
+
name: second,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
printUsage();
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const { shortcut, name } = parseArgs(args);
|
|
1114
|
+
|
|
1115
|
+
async function main() {
|
|
1116
|
+
switch (shortcut) {
|
|
1117
|
+
case "svc":
|
|
1118
|
+
if (!name) {
|
|
1119
|
+
console.error(`${C.yellow}❌ Please provide a service name.${C.reset}`);
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
createService(name);
|
|
1123
|
+
break;
|
|
1124
|
+
|
|
1125
|
+
case "ctx":
|
|
1126
|
+
if (!name) {
|
|
1127
|
+
console.error(`${C.yellow}❌ Please provide a context name.${C.reset}`);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
createContext(name);
|
|
1131
|
+
break;
|
|
1132
|
+
|
|
1133
|
+
case "gc":
|
|
1134
|
+
case "cc":
|
|
1135
|
+
if (!name) {
|
|
1136
|
+
console.error(
|
|
1137
|
+
`${C.yellow}❌ Please provide a component name.${C.reset}`,
|
|
1138
|
+
);
|
|
1139
|
+
process.exit(1);
|
|
1140
|
+
}
|
|
1141
|
+
createComponent(name);
|
|
1142
|
+
break;
|
|
1143
|
+
|
|
1144
|
+
case "app":
|
|
1145
|
+
if (!name) {
|
|
1146
|
+
console.error(`${C.yellow}❌ Please provide an app name.${C.reset}`);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
await createApp(name);
|
|
1150
|
+
break;
|
|
1151
|
+
|
|
1152
|
+
default:
|
|
1153
|
+
console.error(`${C.yellow}❌ Unknown shortcut.${C.reset}`);
|
|
1154
|
+
printUsage();
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
main().catch((err) => {
|
|
1160
|
+
console.error(`${C.yellow}❌ Unexpected error:${C.reset}`, err);
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
});
|