fastnode-cli 0.6.0 → 0.6.2

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 (2) hide show
  1. package/dist/index.js +284 -99
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -7,160 +7,265 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const child_process_1 = require("child_process");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
- const [, , command, resource, name] = process.argv;
10
+ const [, , command, resource, name, moduleName] = process.argv;
11
11
  if (command === "create" && resource) {
12
12
  createProject(resource);
13
13
  }
14
14
  else if (command === "serve" && resource) {
15
15
  serveProject(resource);
16
16
  }
17
+ else if (command === "build") {
18
+ buildProject();
19
+ }
17
20
  else if (command === "generate" && resource === "controller" && name) {
18
- generateController(name);
21
+ generateController(name, moduleName);
19
22
  }
20
23
  else {
21
24
  printUsage();
22
25
  }
23
26
  function createProject(projectName) {
24
27
  const projectDir = path_1.default.resolve(projectName);
25
- const moduleDir = path_1.default.join(projectDir, "src", projectName);
26
- fs_1.default.mkdirSync(moduleDir, { recursive: true });
27
- fs_1.default.writeFileSync(path_1.default.join(moduleDir, "main.ts"), buildMainTemplate(projectName));
28
+ const srcDir = path_1.default.join(projectDir, "src");
29
+ const constantsDir = path_1.default.join(srcDir, "constants");
30
+ const controllersDir = path_1.default.join(srcDir, "controllers");
31
+ const servicesDir = path_1.default.join(srcDir, "services");
32
+ const helpersDir = path_1.default.join(srcDir, "helpers");
33
+ fs_1.default.mkdirSync(constantsDir, { recursive: true });
34
+ fs_1.default.mkdirSync(controllersDir, { recursive: true });
35
+ fs_1.default.mkdirSync(servicesDir, { recursive: true });
36
+ fs_1.default.mkdirSync(helpersDir, { recursive: true });
37
+ fs_1.default.writeFileSync(path_1.default.join(srcDir, "main.ts"), buildMainTemplate());
38
+ fs_1.default.writeFileSync(path_1.default.join(srcDir, "app.module.ts"), buildAppModuleTemplate(projectName));
39
+ fs_1.default.writeFileSync(path_1.default.join(controllersDir, "app.controller.ts"), buildAppControllerTemplate(projectName));
40
+ fs_1.default.writeFileSync(path_1.default.join(constantsDir, "config.ts"), buildConfigTemplate());
28
41
  fs_1.default.writeFileSync(path_1.default.join(projectDir, "tsconfig.json"), `{
29
42
  "compilerOptions": {
30
- "target": "ES2020",
31
- "module": "CommonJS",
32
- "rootDir": "./src",
33
- "outDir": "./dist",
43
+ "target": "ES2022",
44
+ "module": "es2022",
45
+ "moduleResolution": "bundler",
46
+ "rootDir": "src",
47
+ "outDir": "dist",
34
48
  "strict": true,
35
49
  "esModuleInterop": true,
36
50
  "experimentalDecorators": true,
37
51
  "emitDecoratorMetadata": true,
38
- "useDefineForClassFields": false
52
+ "skipLibCheck": true,
53
+ "resolveJsonModule": true,
54
+ "types": ["node"]
39
55
  },
40
- "include": ["src"]
56
+ "include": ["src/**/*.ts"],
57
+ "exclude": ["dist", "node_modules"]
41
58
  }
42
59
  `);
43
60
  fs_1.default.writeFileSync(path_1.default.join(projectDir, "package.json"), `{
44
61
  "name": "${projectName}",
45
62
  "version": "1.0.0",
46
- "type": "commonjs",
47
- "main": "dist/index.js",
63
+ "type": "module",
48
64
  "scripts": {
49
- "dev": "tsx src/${projectName}/main.ts",
50
- "build": "tsc",
51
- "start": "node dist/${projectName}/main.js"
65
+ "dev": "tsx watch src/main.ts",
66
+ "build": "tsc -p tsconfig.json",
67
+ "start": "node dist/main.js"
52
68
  },
53
69
  "dependencies": {
54
- "fastnode-core": "latest",
55
- "reflect-metadata": "^0.1.13"
70
+ "fastnode-core": "^0.6.1",
71
+ "reflect-metadata": "^0.2.2"
56
72
  },
57
73
  "devDependencies": {
58
- "tsx": "^4.7.0",
59
- "typescript": "^5.0.0"
74
+ "@types/node": "^24.12.0",
75
+ "tsx": "^4.21.0",
76
+ "typescript": "^5.9.3"
60
77
  }
61
78
  }
62
79
  `);
63
- console.log(`✔ Project '${projectName}' created at ${projectDir}`);
80
+ console.log(`Project '${projectName}' created at ${projectDir}`);
64
81
  }
65
82
  function serveProject(projectName) {
66
- const entry = path_1.default.resolve(process.cwd(), `src/${projectName}/main.ts`);
83
+ const entry = path_1.default.resolve(process.cwd(), "src", "main.ts");
67
84
  if (!fs_1.default.existsSync(entry)) {
68
- console.error(`❌ Cannot find module at ${entry}`);
69
- process.exit(1);
85
+ fail(`Cannot find module at ${entry}`);
70
86
  }
71
87
  (0, child_process_1.execSync)(`npx tsx ${entry}`, { stdio: "inherit" });
72
88
  }
73
- function generateController(controllerName) {
89
+ function buildProject() {
90
+ const errors = validateProjectConfiguration();
91
+ if (errors.length > 0) {
92
+ printValidationErrors(errors);
93
+ process.exit(1);
94
+ }
95
+ try {
96
+ clearBuildOutput();
97
+ (0, child_process_1.execSync)("npx tsc -p tsconfig.json", { stdio: "inherit" });
98
+ console.log("Build completed successfully.");
99
+ }
100
+ catch (error) {
101
+ console.error("Build failed.");
102
+ if (error instanceof Error && error.message) {
103
+ console.error(error.message);
104
+ }
105
+ process.exit(1);
106
+ }
107
+ }
108
+ function clearBuildOutput() {
109
+ const distDir = path_1.default.resolve(process.cwd(), "dist");
110
+ if (!fs_1.default.existsSync(distDir)) {
111
+ return;
112
+ }
113
+ fs_1.default.rmSync(distDir, { recursive: true, force: true });
114
+ }
115
+ function generateController(controllerName, moduleName) {
74
116
  const normalizedName = toKebabCase(controllerName);
75
117
  const className = `${toPascalCase(controllerName)}Controller`;
76
- const controllerDir = path_1.default.resolve(process.cwd(), "src", normalizedName);
118
+ const moduleFile = resolveModuleFile(moduleName);
119
+ const controllerDir = path_1.default.resolve(process.cwd(), "src", "controllers");
77
120
  const controllerFile = path_1.default.join(controllerDir, `${normalizedName}.controller.ts`);
78
121
  fs_1.default.mkdirSync(controllerDir, { recursive: true });
79
122
  if (fs_1.default.existsSync(controllerFile)) {
80
- console.error(`❌ Controller already exists at ${controllerFile}`);
81
- process.exit(1);
123
+ fail(`Controller already exists at ${controllerFile}`);
82
124
  }
83
125
  fs_1.default.writeFileSync(controllerFile, buildControllerTemplate(normalizedName));
84
- console.log(`✔ Controller '${normalizedName}' created at ${controllerFile}`);
85
- console.log(`ℹ Add ${className} to a module so FastNode can register it.`);
126
+ updateModuleControllers(moduleFile, className, normalizedName);
127
+ console.log(`Controller '${normalizedName}' created at ${controllerFile}`);
128
+ console.log(`Added ${className} to ${path_1.default.basename(moduleFile)}.`);
86
129
  }
87
- function buildMainTemplate(projectName) {
88
- const className = `${toPascalCase(projectName)}Controller`;
89
- const moduleName = `${toPascalCase(projectName)}Module`;
130
+ function validateProjectConfiguration() {
131
+ const errors = [];
132
+ const packageJsonPath = path_1.default.resolve(process.cwd(), "package.json");
133
+ const tsconfigPath = path_1.default.resolve(process.cwd(), "tsconfig.json");
134
+ const mainFilePath = path_1.default.resolve(process.cwd(), "src", "main.ts");
135
+ const packageJson = readJsonFile(packageJsonPath, "package.json", errors);
136
+ const tsconfig = readJsonFile(tsconfigPath, "tsconfig.json", errors);
137
+ if (!fs_1.default.existsSync(mainFilePath)) {
138
+ errors.push(`Missing entry file: ${mainFilePath}`);
139
+ }
140
+ validatePackageJson(packageJson, errors);
141
+ validateTsconfig(tsconfig, errors);
142
+ return errors;
143
+ }
144
+ function validatePackageJson(packageJson, errors) {
145
+ if (!packageJson) {
146
+ return;
147
+ }
148
+ const scripts = asRecord(packageJson.scripts);
149
+ const dependencies = asRecord(packageJson.dependencies);
150
+ const devDependencies = asRecord(packageJson.devDependencies);
151
+ if (packageJson.type !== "module") {
152
+ errors.push(`package.json: expected "type" to be "module".`);
153
+ }
154
+ if (typeof scripts?.build !== "string" || !scripts.build.includes("tsc")) {
155
+ errors.push(`package.json: expected scripts.build to run TypeScript, for example "tsc -p tsconfig.json".`);
156
+ }
157
+ if (typeof dependencies?.["fastnode-core"] !== "string") {
158
+ errors.push(`package.json: missing dependency "fastnode-core".`);
159
+ }
160
+ else if (!hasLocalInstalledPackage("fastnode-core")) {
161
+ errors.push(`package.json: dependency "fastnode-core" is not installed. Run npm install.`);
162
+ }
163
+ if (typeof dependencies?.["reflect-metadata"] !== "string") {
164
+ errors.push(`package.json: missing dependency "reflect-metadata".`);
165
+ }
166
+ else if (!hasLocalInstalledPackage("reflect-metadata")) {
167
+ errors.push(`package.json: dependency "reflect-metadata" is not installed. Run npm install.`);
168
+ }
169
+ if (typeof devDependencies?.typescript !== "string") {
170
+ errors.push(`package.json: missing devDependency "typescript".`);
171
+ }
172
+ else if (!hasLocalInstalledPackage("typescript")) {
173
+ errors.push(`package.json: devDependency "typescript" is not installed. Run npm install.`);
174
+ }
175
+ }
176
+ function validateTsconfig(tsconfig, errors) {
177
+ if (!tsconfig) {
178
+ return;
179
+ }
180
+ const compilerOptions = asRecord(tsconfig.compilerOptions);
181
+ if (!compilerOptions) {
182
+ errors.push(`tsconfig.json: missing compilerOptions.`);
183
+ return;
184
+ }
185
+ if (compilerOptions.rootDir !== "src") {
186
+ errors.push(`tsconfig.json: expected compilerOptions.rootDir to be "src".`);
187
+ }
188
+ if (compilerOptions.outDir !== "dist") {
189
+ errors.push(`tsconfig.json: expected compilerOptions.outDir to be "dist".`);
190
+ }
191
+ if (compilerOptions.moduleResolution !== "bundler") {
192
+ errors.push(`tsconfig.json: expected compilerOptions.moduleResolution to be "bundler".`);
193
+ }
194
+ if (compilerOptions.experimentalDecorators !== true) {
195
+ errors.push(`tsconfig.json: expected compilerOptions.experimentalDecorators to be true.`);
196
+ }
197
+ if (compilerOptions.emitDecoratorMetadata !== true) {
198
+ errors.push(`tsconfig.json: expected compilerOptions.emitDecoratorMetadata to be true.`);
199
+ }
200
+ if (compilerOptions.module !== "es2022" &&
201
+ compilerOptions.module !== "ES2022") {
202
+ errors.push(`tsconfig.json: expected compilerOptions.module to be "es2022".`);
203
+ }
204
+ }
205
+ function readJsonFile(filePath, label, errors) {
206
+ if (!fs_1.default.existsSync(filePath)) {
207
+ errors.push(`Missing ${label} at ${filePath}`);
208
+ return undefined;
209
+ }
210
+ try {
211
+ const content = fs_1.default.readFileSync(filePath, "utf8");
212
+ return JSON.parse(content);
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : "Unknown JSON parsing error.";
216
+ errors.push(`Invalid ${label}: ${message}`);
217
+ return undefined;
218
+ }
219
+ }
220
+ function printValidationErrors(errors) {
221
+ console.error("FastNode build validation failed.");
222
+ console.error("");
223
+ for (const error of errors) {
224
+ console.error(`- ${error}`);
225
+ }
226
+ console.error("");
227
+ console.error("Update your package.json and tsconfig.json, then run `fastnode build` again.");
228
+ }
229
+ function buildMainTemplate() {
90
230
  return `import "reflect-metadata";
91
- import {
92
- Body,
93
- Context,
94
- Controller,
95
- EmitToClient,
96
- ExecutionPolicy,
97
- Get,
98
- Module,
99
- Param,
100
- Post,
101
- Query,
102
- SocketController,
103
- Subscribe,
104
- createApp,
105
- } from "fastnode-core";
106
-
107
- @Controller("/${projectName}")
108
- @ExecutionPolicy({ timeout: 5000 })
109
- class ${className} {
110
- @Get("/")
111
- async hello(
112
- @Query("name") name?: string,
113
- @Context() ctx?: any
114
- ) {
115
- await ctx?.sleep(25);
231
+ import { createApp } from "fastnode-core";
232
+ import { AppModule } from "./app.module.js";
233
+ import { API_CONFIG } from "./constants/config.js";
116
234
 
117
- return {
118
- message: \`Hello from ${projectName}\${name ? \`, \${name}\` : ""}\`,
119
- deadline: ctx?.snapshot?.().deadline,
120
- };
121
- }
235
+ export const app = createApp([AppModule], {
122
236
 
123
- @Get("/{item_id}")
124
- getOne(
125
- @Param("item_id") itemId: number,
126
- @Query("include") include?: string
127
- ) {
128
- return {
129
- item_id: itemId,
130
- include,
131
- };
132
- }
237
+ }).listen({ port: API_CONFIG.port, host: API_CONFIG.host });
238
+ `;
239
+ }
240
+ function buildAppModuleTemplate(projectName) {
241
+ const controllerName = `${toPascalCase(projectName)}Controller`;
242
+ return `import { Module } from "fastnode-core";
243
+ import { ${controllerName} } from "./controllers/app.controller.js";
133
244
 
134
- @Post("/")
135
- create() {
136
- return {
137
- created: true,
138
- };
139
- }
245
+ @Module({ controllers: [${controllerName}] })
246
+ export class AppModule {}
247
+ `;
140
248
  }
249
+ function buildAppControllerTemplate(projectName) {
250
+ const className = `${toPascalCase(projectName)}Controller`;
251
+ return `import { Controller, Get } from "fastnode-core";
141
252
 
142
- @SocketController("/chat")
143
- class ${toPascalCase(projectName)}SocketController {
144
- @Subscribe("ping")
145
- @EmitToClient()
146
- ping(@Body("message") message?: string) {
253
+ @Controller("/")
254
+ export class ${className} {
255
+ @Get("/")
256
+ getHello() {
147
257
  return {
148
- message: message ?? "pong",
258
+ message: "Hello from ${projectName}",
149
259
  };
150
260
  }
151
261
  }
152
-
153
- @Module({ controllers: [${className}, ${toPascalCase(projectName)}SocketController] })
154
- class ${moduleName} {}
155
-
156
- // Drop files into ./static to serve them automatically, for example static/logo.png -> /logo.png.
157
- // Upload routes can use @Upload() and @File() to save files under ./static/uploads by default.
158
- createApp([${moduleName}], {
159
- executionPolicy: {
160
- timeout: 30000,
161
- mode: "inline",
162
- },
163
- }).listen(3000);
262
+ `;
263
+ }
264
+ function buildConfigTemplate() {
265
+ return `export const API_CONFIG = {
266
+ host: "0.0.0.0",
267
+ port: 3000,
268
+ };
164
269
  `;
165
270
  }
166
271
  function buildControllerTemplate(controllerName) {
@@ -169,8 +274,8 @@ function buildControllerTemplate(controllerName) {
169
274
  Body,
170
275
  Controller,
171
276
  Get,
172
- Post,
173
277
  Param,
278
+ Post,
174
279
  Query,
175
280
  } from "fastnode-core";
176
281
 
@@ -212,6 +317,85 @@ export class ${className} {
212
317
  }
213
318
  `;
214
319
  }
320
+ function resolveModuleFile(moduleName) {
321
+ const srcDir = path_1.default.resolve(process.cwd(), "src");
322
+ if (!fs_1.default.existsSync(srcDir)) {
323
+ fail(`Cannot find src directory at ${srcDir}`);
324
+ }
325
+ if (!moduleName) {
326
+ const defaultModuleFile = path_1.default.join(srcDir, "app.module.ts");
327
+ if (!fs_1.default.existsSync(defaultModuleFile)) {
328
+ fail(`Cannot find default module at ${defaultModuleFile}`);
329
+ }
330
+ return defaultModuleFile;
331
+ }
332
+ const expectedFileName = `${toKebabCase(moduleName)}.module.ts`;
333
+ const moduleFile = findFileRecursive(srcDir, expectedFileName);
334
+ if (!moduleFile) {
335
+ fail(`Cannot find module '${moduleName}'. Expected ${expectedFileName} under ${srcDir}`);
336
+ }
337
+ return moduleFile;
338
+ }
339
+ function findFileRecursive(dir, fileName) {
340
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
341
+ for (const entry of entries) {
342
+ const fullPath = path_1.default.join(dir, entry.name);
343
+ if (entry.isDirectory()) {
344
+ const match = findFileRecursive(fullPath, fileName);
345
+ if (match) {
346
+ return match;
347
+ }
348
+ }
349
+ else if (entry.isFile() && entry.name === fileName) {
350
+ return fullPath;
351
+ }
352
+ }
353
+ return undefined;
354
+ }
355
+ function updateModuleControllers(moduleFile, controllerClassName, controllerName) {
356
+ const controllerFile = path_1.default.resolve(process.cwd(), "src", "controllers", `${controllerName}.controller.ts`);
357
+ const importStatement = `import { ${controllerClassName} } from "${buildRelativeImportPath(moduleFile, controllerFile)}";`;
358
+ let moduleSource = fs_1.default.readFileSync(moduleFile, "utf8");
359
+ if (!moduleSource.includes(importStatement)) {
360
+ moduleSource = `${importStatement}\n${moduleSource}`;
361
+ }
362
+ const controllersMatch = moduleSource.match(/controllers\s*:\s*\[([\s\S]*?)\]/);
363
+ if (!controllersMatch) {
364
+ fail(`Could not find a controllers array inside ${moduleFile}`);
365
+ }
366
+ const existingControllers = controllersMatch[1]
367
+ .split(",")
368
+ .map((value) => value.trim())
369
+ .filter(Boolean);
370
+ if (!existingControllers.includes(controllerClassName)) {
371
+ existingControllers.push(controllerClassName);
372
+ }
373
+ moduleSource = moduleSource.replace(/controllers\s*:\s*\[[\s\S]*?\]/, `controllers: [${existingControllers.join(", ")}]`);
374
+ fs_1.default.writeFileSync(moduleFile, moduleSource);
375
+ }
376
+ function buildRelativeImportPath(fromFile, toFile) {
377
+ const relativePath = path_1.default
378
+ .relative(path_1.default.dirname(fromFile), toFile)
379
+ .replace(/\\/g, "/")
380
+ .replace(/\.ts$/, ".js");
381
+ if (relativePath.startsWith(".")) {
382
+ return relativePath;
383
+ }
384
+ return `./${relativePath}`;
385
+ }
386
+ function asRecord(value) {
387
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
388
+ return undefined;
389
+ }
390
+ return value;
391
+ }
392
+ function hasLocalInstalledPackage(packageName) {
393
+ return fs_1.default.existsSync(path_1.default.resolve(process.cwd(), "node_modules", packageName));
394
+ }
395
+ function fail(message) {
396
+ console.error(message);
397
+ process.exit(1);
398
+ }
215
399
  function capitalize(value) {
216
400
  return value.charAt(0).toUpperCase() + value.slice(1);
217
401
  }
@@ -234,6 +418,7 @@ function printUsage() {
234
418
  Usage:
235
419
  fastnode create <project-name>
236
420
  fastnode serve <project-name>
237
- fastnode generate controller <controller-name>
421
+ fastnode build
422
+ fastnode generate controller <controller-name> [module-name]
238
423
  `);
239
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastnode-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "The official CLI for scaffolding and serving FastNode applications.",
5
5
  "bin": {
6
6
  "fastnode": "dist/index.js"
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "chalk": "^5.3.0",
27
27
  "commander": "^11.0.0",
28
- "fastnode-core": "^0.6.0",
28
+ "fastnode-core": "^0.6.1",
29
29
  "reflect-metadata": "^0.1.13"
30
30
  },
31
31
  "devDependencies": {