@exanderal/stackcraft 0.1.1 → 0.1.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 (56) hide show
  1. package/README.md +54 -7
  2. package/dist/create/index.d.ts.map +1 -1
  3. package/dist/create/index.js +12 -0
  4. package/dist/create/index.js.map +1 -1
  5. package/dist/create/scaffolders/__tests__/web-vite.test.js +1 -0
  6. package/dist/create/scaffolders/__tests__/web-vite.test.js.map +1 -1
  7. package/dist/create/scaffolders/api-nestjs-rest.d.ts.map +1 -1
  8. package/dist/create/scaffolders/api-nestjs-rest.js +19 -2
  9. package/dist/create/scaffolders/api-nestjs-rest.js.map +1 -1
  10. package/dist/create/types.d.ts +2 -0
  11. package/dist/create/types.d.ts.map +1 -1
  12. package/package.json +1 -1
  13. package/templates/api-nestjs-rest/package.json +5 -2
  14. package/templates/api-nestjs-rest/project.json +1 -1
  15. package/templates/api-nestjs-rest/src/app.module.ts +8 -5
  16. package/templates/api-nestjs-rest/src/common/entities/base.entity.ts +16 -0
  17. package/templates/api-nestjs-rest/src/common/repositories/entity.repository.ts +21 -0
  18. package/templates/api-nestjs-rest/src/common/repositories/readonly-entity.repository.ts +22 -0
  19. package/templates/api-nestjs-rest/src/common/services/entity.service.ts +24 -0
  20. package/templates/api-nestjs-rest/src/common/services/readonly-entity.service.ts +23 -0
  21. package/templates/api-nestjs-rest/src/modules/database/database.module.ts +18 -0
  22. package/templates/api-nestjs-rest/src/modules/health/__tests__/health.controller.spec.ts +20 -0
  23. package/templates/api-nestjs-rest/src/modules/health/__tests__/health.service.spec.ts +18 -0
  24. package/templates/api-nestjs-rest/src/modules/health/health.controller.ts +12 -0
  25. package/templates/api-nestjs-rest/src/modules/health/health.module.ts +9 -0
  26. package/templates/api-nestjs-rest/src/modules/health/health.service.ts +8 -0
  27. package/templates/api-nestjs-rest/test/app.e2e-spec.ts +9 -5
  28. package/templates/base/package.json +3 -1
  29. package/templates/base/pnpm-workspace.yaml +1 -0
  30. package/templates/base/tools/generators/generators.json +9 -0
  31. package/templates/base/tools/generators/module/crud-files/__fileName__.controller.ts__tmpl__ +36 -0
  32. package/templates/base/tools/generators/module/files/__fileName__.model.ts__tmpl__ +8 -0
  33. package/templates/base/tools/generators/module/files/__fileName__.module.ts__tmpl__ +12 -0
  34. package/templates/base/tools/generators/module/files/__fileName__.repository.ts__tmpl__ +15 -0
  35. package/templates/base/tools/generators/module/files/__fileName__.service.ts__tmpl__ +11 -0
  36. package/templates/base/tools/generators/module/files/__tests__/__fileName__.integration.spec.ts__tmpl__ +31 -0
  37. package/templates/base/tools/generators/module/index.js +44 -0
  38. package/templates/base/tools/generators/module/schema.json +24 -0
  39. package/templates/base/tools/generators/package.json +6 -0
  40. package/templates/web-nextjs/package.json +3 -0
  41. package/templates/web-nextjs/postcss.config.mjs +7 -0
  42. package/templates/web-nextjs/src/app/globals.css +1 -0
  43. package/templates/web-nextjs/src/components/TemplateComponent/TemplateComponent.behaviour.ts +39 -0
  44. package/templates/web-nextjs/src/components/TemplateComponent/TemplateComponent.tsx +14 -0
  45. package/templates/web-vite/package.json +2 -0
  46. package/templates/web-vite/src/components/TemplateComponent/TemplateComponent.behaviour.ts +39 -0
  47. package/templates/web-vite/src/components/TemplateComponent/TemplateComponent.tsx +14 -0
  48. package/templates/web-vite/src/index.css +1 -8
  49. package/templates/web-vite/vite.config.ts +3 -3
  50. package/templates/api-nestjs-rest/src/app.controller.spec.ts +0 -22
  51. package/templates/api-nestjs-rest/src/app.controller.ts +0 -12
  52. package/templates/api-nestjs-rest/src/app.service.ts +0 -8
  53. package/templates/web-nextjs/app/globals.css +0 -10
  54. /package/templates/web-nextjs/{app → src/app}/favicon.ico +0 -0
  55. /package/templates/web-nextjs/{app → src/app}/layout.tsx +0 -0
  56. /package/templates/web-nextjs/{app → src/app}/page.tsx +0 -0
package/README.md CHANGED
@@ -10,21 +10,68 @@ Spin up a production-ready monorepo in one command.
10
10
  npx @exanderal/stackcraft
11
11
  ```
12
12
 
13
- Follow the prompts. You'll end up with an Nx monorepo with a NestJS API and your choice of Vite + React or Next.js frontend — deps installed, ready to run.
13
+ Follow the prompts — you'll have an Nx monorepo with deps installed and ready to run.
14
14
 
15
15
  ## What you get
16
16
 
17
- - Nx monorepo with `apps/` and `packages/`
18
- - NestJS REST API (`apps/api`)
19
- - Vite + React or Next.js (`apps/web`)
20
- - Every app has `dev`, `build`, `lint` scripts wired into Nx
17
+ ```
18
+ your-project/
19
+ ├── apps/
20
+ │ ├── backend/ # NestJS REST API
21
+ │ └── web/ # Vite + React or Next.js
22
+ ├── packages/ # shared code
23
+ └── tools/
24
+ └── generators/ # local Nx code generators
25
+ ```
26
+
27
+ ### Backend (`apps/backend`)
28
+
29
+ - NestJS with TypeORM
30
+ - PostgreSQL or MySQL
31
+ - Repository/Service abstraction layer (`EntityRepository`, `EntityService`)
32
+ - Module-based structure under `src/modules/` — each module owns its model, repository, and service
33
+ - HTTP controllers live separately in `src/api/`
34
+
35
+ ### Frontend (`apps/web`)
36
+
37
+ - Vite + React or Next.js
38
+ - Tailwind CSS v4
39
+ - TypeScript
40
+
41
+ ## Running the project
42
+
43
+ Each app has the same scripts:
44
+
45
+ ```sh
46
+ pnpm dev # start all apps in parallel
47
+ pnpm build # build all apps
48
+ pnpm test # run all tests
49
+ pnpm lint # lint all apps
50
+ ```
51
+
52
+ Or target a specific app:
53
+
54
+ ```sh
55
+ pnpm nx run backend:dev
56
+ pnpm nx run web:dev
57
+ ```
58
+
59
+ ## Code generation
60
+
61
+ Generate a new backend module:
62
+
63
+ ```sh
64
+ pnpm generate:module --name=trainer
65
+ ```
66
+
67
+ This creates `apps/backend/src/modules/trainer/` with a model, repository, service, module, and integration test. Add `--crud` to also generate a CRUD controller in `src/api/trainer/`.
21
68
 
22
69
  ## Stack
23
70
 
24
71
  - **Monorepo** — Nx
25
72
  - **Package manager** — pnpm or npm
26
- - **Backend** — NestJS
27
- - **Frontend** — Vite + React or Next.js
73
+ - **Backend** — NestJS, TypeORM, PostgreSQL/MySQL
74
+ - **Frontend** — Vite + React or Next.js, Tailwind CSS v4
28
75
 
29
76
  ## Roadmap
30
77
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,kBAmD3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,kBAgE3B"}
@@ -23,6 +23,17 @@ export async function create() {
23
23
  cancel('Cancelled.');
24
24
  process.exit(0);
25
25
  }
26
+ const database = await select({
27
+ message: 'Database',
28
+ options: [
29
+ { value: 'postgres', label: 'PostgreSQL', hint: 'recommended' },
30
+ { value: 'mysql', label: 'MySQL' },
31
+ ],
32
+ });
33
+ if (isCancel(database)) {
34
+ cancel('Cancelled.');
35
+ process.exit(0);
36
+ }
26
37
  const packageManager = await select({
27
38
  message: 'Package manager',
28
39
  options: [
@@ -38,6 +49,7 @@ export async function create() {
38
49
  projectName: projectName,
39
50
  frontend: frontend,
40
51
  backend: 'nestjs-rest',
52
+ database: database,
41
53
  packageManager: packageManager,
42
54
  targetDir: resolve(process.cwd(), projectName),
43
55
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAA;AACtF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAGxC,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,KAAK,CAAC,kDAAkD,CAAC,CAAA;IAEzD,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC;QAC7B,OAAO,EAAE,cAAc;QACvB,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;KAC/C,CAAC,CAAA;IACF,IAAI,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1B,MAAM,CAAC,YAAY,CAAC,CAAA;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC;QAC5B,OAAO,EAAE,UAAU;QACnB,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,iCAAiC,EAAE;YACjF,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,+BAA+B,EAAE;SAC7E;KACF,CAAC,CAAA;IACF,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,MAAM,CAAC,YAAY,CAAC,CAAA;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC;QAClC,OAAO,EAAE,iBAAiB;QAC1B,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE;YACrD,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE;SAC/B;KACF,CAAC,CAAA;IACF,IAAI,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,YAAY,CAAC,CAAA;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,MAAM,MAAM,GAAkB;QAC5B,WAAW,EAAE,WAAqB;QAClC,QAAQ,EAAE,QAAoB;QAC9B,OAAO,EAAE,aAAwB;QACjC,cAAc,EAAE,cAAgC;QAChD,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,WAAqB,CAAC;KACzD,CAAA;IAED,MAAM,CAAC,GAAG,OAAO,EAAE,CAAA;IACnB,CAAC,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAA;IACtC,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/C,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAEf,KAAK,CAAC,MAAM,MAAM,CAAC,WAAW,OAAO,MAAM,CAAC,cAAc,MAAM,CAAC,CAAA;AACnE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ const CONFIG = {
10
10
  projectName: 'my-app',
11
11
  frontend: 'vite',
12
12
  backend: 'nestjs-rest',
13
+ database: 'postgres',
13
14
  packageManager: 'pnpm',
14
15
  targetDir: '/tmp/my-app',
15
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"web-vite.test.js","sourceRoot":"","sources":["../../../../src/create/scaffolders/__tests__/web-vite.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE5D,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,CAAC;IACjC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;IAC3C,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;IACxC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACtC,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACvC,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;CAChD,CAAC,CAAC,CAAA;AAEH,MAAM,MAAM,GAAG;IACb,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,MAAe;IACzB,OAAO,EAAE,aAAsB;IAC/B,cAAc,EAAE,MAAe;IAC/B,SAAS,EAAE,aAAa;CACzB,CAAA;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,CAAA;IAEnC,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAClD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;QAEvD,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;QAE1B,MAAM,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAChC,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;QAEvD,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;QAE1B,MAAM,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAC7B,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"web-vite.test.js","sourceRoot":"","sources":["../../../../src/create/scaffolders/__tests__/web-vite.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAE5D,EAAE,CAAC,IAAI,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,CAAC;IACjC,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;IAC3C,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;IACxC,OAAO,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACtC,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,CAAC;IACvC,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC;CAChD,CAAC,CAAC,CAAA;AAEH,MAAM,MAAM,GAAG;IACb,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,MAAe;IACzB,OAAO,EAAE,aAAsB;IAC/B,QAAQ,EAAE,UAAmB;IAC7B,cAAc,EAAE,MAAe;IAC/B,SAAS,EAAE,aAAa;CACzB,CAAA;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC,CAAA;IAEnC,EAAE,CAAC,gCAAgC,EAAE,KAAK,IAAI,EAAE;QAC9C,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAClD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;QAEvD,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;QAE1B,MAAM,CAAC,KAAK,CAAC,CAAC,oBAAoB,CAChC,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,EAAE,EAAE,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAA;QAC/C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;QAEvD,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;QAE1B,MAAM,CAAC,EAAE,CAAC,CAAC,oBAAoB,CAC7B,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,MAAM,CAAC,gBAAgB,CAAC,UAAU,CAAC,EACnC,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"api-nestjs-rest.d.ts","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-rest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,aAAa,iBAO7D"}
1
+ {"version":3,"file":"api-nestjs-rest.d.ts","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-rest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,aAAa,iBAa7D"}
@@ -1,14 +1,31 @@
1
- import { mkdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { copyTemplate } from './utils/copy.js';
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  const TEMPLATES_DIR = join(__dirname, '..', '..', '..', 'templates');
7
+ const DB_CONFIG = {
8
+ postgres: { type: 'postgres', port: '5432', driver: 'pg', driverVersion: '^8.0.0', typesPackage: '@types/pg', typesVersion: '^8.0.0' },
9
+ mysql: { type: 'mysql', port: '3306', driver: 'mysql2', driverVersion: '^3.0.0', typesPackage: null, typesVersion: null },
10
+ };
7
11
  export async function scaffoldNestjsRest(config) {
8
- const appDir = join(config.targetDir, 'apps', 'api');
12
+ const appDir = join(config.targetDir, 'apps', 'backend');
9
13
  await mkdir(appDir, { recursive: true });
14
+ const db = DB_CONFIG[config.database];
10
15
  await copyTemplate(join(TEMPLATES_DIR, 'api-nestjs-rest'), appDir, {
11
16
  projectName: config.projectName,
17
+ dbType: db.type,
18
+ dbPort: db.port,
12
19
  });
20
+ await injectDbDriver(appDir, db);
21
+ }
22
+ async function injectDbDriver(appDir, db) {
23
+ const pkgPath = join(appDir, 'package.json');
24
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
25
+ pkg.dependencies[db.driver] = db.driverVersion;
26
+ if (db.typesPackage) {
27
+ pkg.devDependencies[db.typesPackage] = db.typesVersion;
28
+ }
29
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
13
30
  }
14
31
  //# sourceMappingURL=api-nestjs-rest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-nestjs-rest.js","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACzD,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;AAEpE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAqB;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;IACpD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAExC,MAAM,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE;QACjE,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC,CAAA;AACJ,CAAC"}
1
+ {"version":3,"file":"api-nestjs-rest.js","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACzD,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;AAEpE,MAAM,SAAS,GAAG;IAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE;IACtI,KAAK,EAAK,EAAE,IAAI,EAAE,OAAO,EAAK,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE;CACvH,CAAA;AAEV,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAqB;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;IACxD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAExC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAErC,MAAM,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE;QACjE,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,EAAE,CAAC,IAAI;QACf,MAAM,EAAE,EAAE,CAAC,IAAI;KAChB,CAAC,CAAA;IAEF,MAAM,cAAc,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;AAClC,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,EAA4C;IACxF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;IAExD,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAA;IAE9C,IAAI,EAAE,CAAC,YAAY,EAAE,CAAC;QACpB,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,YAAY,CAAA;IACxD,CAAC;IAED,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAA;AACxE,CAAC"}
@@ -1,10 +1,12 @@
1
1
  export type PackageManager = 'pnpm' | 'npm';
2
2
  export type Frontend = 'vite' | 'nextjs';
3
3
  export type Backend = 'nestjs-rest';
4
+ export type Database = 'postgres' | 'mysql';
4
5
  export interface ProjectConfig {
5
6
  projectName: string;
6
7
  frontend: Frontend;
7
8
  backend: Backend;
9
+ database: Database;
8
10
  packageManager: PackageManager;
9
11
  targetDir: string;
10
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/create/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAA;AAC3C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AACxC,MAAM,MAAM,OAAO,GAAG,aAAa,CAAA;AAEnC,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,cAAc,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/create/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAA;AAC3C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AACxC,MAAM,MAAM,OAAO,GAAG,aAAa,CAAA;AACnC,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;AAE3C,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,cAAc,EAAE,cAAc,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exanderal/stackcraft",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Opinionated full-stack project scaffolding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "{{projectName}}-api",
2
+ "name": "{{projectName}}-backend",
3
3
  "version": "0.0.1",
4
4
  "description": "",
5
5
  "author": "",
@@ -22,10 +22,13 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@nestjs/common": "^11.0.1",
25
+ "@nestjs/config": "^3.0.0",
25
26
  "@nestjs/core": "^11.0.1",
26
27
  "@nestjs/platform-express": "^11.0.1",
28
+ "@nestjs/typeorm": "^10.0.0",
27
29
  "reflect-metadata": "^0.2.2",
28
- "rxjs": "^7.8.1"
30
+ "rxjs": "^7.8.1",
31
+ "typeorm": "^0.3.0"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@eslint/eslintrc": "^3.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "api",
2
+ "name": "backend",
3
3
  "targets": {
4
4
  "dev": {
5
5
  "executor": "nx:run-commands",
@@ -1,10 +1,13 @@
1
1
  import { Module } from '@nestjs/common';
2
- import { AppController } from './app.controller';
3
- import { AppService } from './app.service';
2
+ import { ConfigModule } from '@nestjs/config';
3
+ import { DatabaseModule } from './modules/database/database.module';
4
+ import { HealthModule } from './modules/health/health.module';
4
5
 
5
6
  @Module({
6
- imports: [],
7
- controllers: [AppController],
8
- providers: [AppService],
7
+ imports: [
8
+ ConfigModule.forRoot({ isGlobal: true }),
9
+ DatabaseModule,
10
+ HealthModule,
11
+ ],
9
12
  })
10
13
  export class AppModule {}
@@ -0,0 +1,16 @@
1
+ import {
2
+ CreateDateColumn,
3
+ PrimaryGeneratedColumn,
4
+ UpdateDateColumn,
5
+ } from 'typeorm';
6
+
7
+ export abstract class BaseEntity {
8
+ @PrimaryGeneratedColumn('uuid')
9
+ id: string;
10
+
11
+ @CreateDateColumn()
12
+ createdAt: Date;
13
+
14
+ @UpdateDateColumn()
15
+ updatedAt: Date;
16
+ }
@@ -0,0 +1,21 @@
1
+ import { DeepPartial } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { ReadonlyEntityRepository } from './readonly-entity.repository';
4
+
5
+ export abstract class EntityRepository<
6
+ T extends BaseEntity,
7
+ > extends ReadonlyEntityRepository<T> {
8
+ async create(data: DeepPartial<T>): Promise<T> {
9
+ const entity = this.repo.create(data);
10
+ return this.repo.save(entity);
11
+ }
12
+
13
+ async update(id: string, data: DeepPartial<T>): Promise<T> {
14
+ await this.repo.update(id, data as any);
15
+ return this.findById(id) as Promise<T>;
16
+ }
17
+
18
+ async remove(id: string): Promise<void> {
19
+ await this.repo.delete(id);
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ import { FindManyOptions, FindOptionsWhere, In, Repository } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+
4
+ export abstract class ReadonlyEntityRepository<T extends BaseEntity> {
5
+ constructor(protected readonly repo: Repository<T>) {}
6
+
7
+ findAll(options?: FindManyOptions<T>): Promise<T[]> {
8
+ return this.repo.find(options);
9
+ }
10
+
11
+ findByIds(ids: string[]): Promise<T[]> {
12
+ return this.repo.findBy({ id: In(ids) } as FindOptionsWhere<T>);
13
+ }
14
+
15
+ findById(id: string): Promise<T | null> {
16
+ return this.repo.findOneBy({ id } as FindOptionsWhere<T>);
17
+ }
18
+
19
+ findOne(where: FindOptionsWhere<T>): Promise<T | null> {
20
+ return this.repo.findOneBy(where);
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ import { DeepPartial } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { EntityRepository } from '../repositories/entity.repository';
4
+ import { ReadonlyEntityService } from './readonly-entity.service';
5
+
6
+ export abstract class EntityService<
7
+ T extends BaseEntity,
8
+ > extends ReadonlyEntityService<T> {
9
+ constructor(protected readonly repository: EntityRepository<T>) {
10
+ super(repository);
11
+ }
12
+
13
+ create(data: DeepPartial<T>): Promise<T> {
14
+ return this.repository.create(data);
15
+ }
16
+
17
+ update(id: string, data: DeepPartial<T>): Promise<T> {
18
+ return this.repository.update(id, data);
19
+ }
20
+
21
+ remove(id: string): Promise<void> {
22
+ return this.repository.remove(id);
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { FindManyOptions, FindOptionsWhere } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { ReadonlyEntityRepository } from '../repositories/readonly-entity.repository';
4
+
5
+ export abstract class ReadonlyEntityService<T extends BaseEntity> {
6
+ constructor(protected readonly repository: ReadonlyEntityRepository<T>) {}
7
+
8
+ findAll(options?: FindManyOptions<T>): Promise<T[]> {
9
+ return this.repository.findAll(options);
10
+ }
11
+
12
+ findByIds(ids: string[]): Promise<T[]> {
13
+ return this.repository.findByIds(ids);
14
+ }
15
+
16
+ findById(id: string): Promise<T | null> {
17
+ return this.repository.findById(id);
18
+ }
19
+
20
+ findOne(where: FindOptionsWhere<T>): Promise<T | null> {
21
+ return this.repository.findOne(where);
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule } from '@nestjs/typeorm';
3
+
4
+ @Module({
5
+ imports: [
6
+ TypeOrmModule.forRoot({
7
+ type: '{{dbType}}' as 'postgres' | 'mysql',
8
+ host: process.env.DB_HOST ?? 'localhost',
9
+ port: parseInt(process.env.DB_PORT ?? '{{dbPort}}', 10),
10
+ username: process.env.DB_USER ?? 'postgres',
11
+ password: process.env.DB_PASSWORD ?? '',
12
+ database: process.env.DB_NAME ?? '{{projectName}}',
13
+ entities: [],
14
+ synchronize: process.env.NODE_ENV !== 'production',
15
+ }),
16
+ ],
17
+ })
18
+ export class DatabaseModule {}
@@ -0,0 +1,20 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { HealthController } from './health.controller';
3
+ import { HealthService } from './health.service';
4
+
5
+ describe('HealthController', () => {
6
+ let controller: HealthController;
7
+
8
+ beforeEach(async () => {
9
+ const module: TestingModule = await Test.createTestingModule({
10
+ controllers: [HealthController],
11
+ providers: [HealthService],
12
+ }).compile();
13
+
14
+ controller = module.get<HealthController>(HealthController);
15
+ });
16
+
17
+ it('returns ok status', () => {
18
+ expect(controller.check()).toEqual({ status: 'ok' });
19
+ });
20
+ });
@@ -0,0 +1,18 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { HealthService } from './health.service';
3
+
4
+ describe('HealthService', () => {
5
+ let service: HealthService;
6
+
7
+ beforeEach(async () => {
8
+ const module: TestingModule = await Test.createTestingModule({
9
+ providers: [HealthService],
10
+ }).compile();
11
+
12
+ service = module.get<HealthService>(HealthService);
13
+ });
14
+
15
+ it('returns ok status', () => {
16
+ expect(service.check()).toEqual({ status: 'ok' });
17
+ });
18
+ });
@@ -0,0 +1,12 @@
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import { HealthService } from './health.service';
3
+
4
+ @Controller('health')
5
+ export class HealthController {
6
+ constructor(private readonly healthService: HealthService) {}
7
+
8
+ @Get()
9
+ check() {
10
+ return this.healthService.check();
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { HealthController } from './health.controller';
3
+ import { HealthService } from './health.service';
4
+
5
+ @Module({
6
+ controllers: [HealthController],
7
+ providers: [HealthService],
8
+ })
9
+ export class HealthModule {}
@@ -0,0 +1,8 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class HealthService {
5
+ check() {
6
+ return { status: 'ok' };
7
+ }
8
+ }
@@ -1,10 +1,10 @@
1
- import { Test, TestingModule } from '@nestjs/testing';
2
1
  import { INestApplication } from '@nestjs/common';
2
+ import { Test, TestingModule } from '@nestjs/testing';
3
3
  import * as request from 'supertest';
4
4
  import { App } from 'supertest/types';
5
5
  import { AppModule } from './../src/app.module';
6
6
 
7
- describe('AppController (e2e)', () => {
7
+ describe('App (e2e)', () => {
8
8
  let app: INestApplication<App>;
9
9
 
10
10
  beforeEach(async () => {
@@ -16,10 +16,14 @@ describe('AppController (e2e)', () => {
16
16
  await app.init();
17
17
  });
18
18
 
19
- it('/ (GET)', () => {
19
+ afterEach(async () => {
20
+ await app.close();
21
+ });
22
+
23
+ it('/health (GET)', () => {
20
24
  return request(app.getHttpServer())
21
- .get('/')
25
+ .get('/health')
22
26
  .expect(200)
23
- .expect('Hello World!');
27
+ .expect({ status: 'ok' });
24
28
  });
25
29
  });
@@ -6,9 +6,11 @@
6
6
  "dev": "nx run-many -t dev",
7
7
  "build": "nx run-many -t build",
8
8
  "test": "nx run-many -t test",
9
- "lint": "nx run-many -t lint"
9
+ "lint": "nx run-many -t lint",
10
+ "generate:module": "nx g @local/generators:module"
10
11
  },
11
12
  "devDependencies": {
13
+ "@nx/devkit": "^20.0.0",
12
14
  "nx": "^20.0.0"
13
15
  }
14
16
  }
@@ -1,3 +1,4 @@
1
1
  packages:
2
2
  - 'apps/*'
3
3
  - 'packages/*' # shared code: graphql types, ui components, etc.
4
+ - 'tools/generators'
@@ -0,0 +1,9 @@
1
+ {
2
+ "generators": {
3
+ "module": {
4
+ "factory": "./module/index",
5
+ "schema": "./module/schema.json",
6
+ "description": "Create a NestJS module"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ Controller,
3
+ Delete,
4
+ Get,
5
+ Param,
6
+ ParseUUIDPipe,
7
+ Query,
8
+ } from '@nestjs/common';
9
+ import { FindManyOptions } from 'typeorm';
10
+ import { <%= className %>Model } from '../../modules/<%= fileName %>/<%= fileName %>.model';
11
+ import { <%= className %>Service } from '../../modules/<%= fileName %>/<%= fileName %>.service';
12
+
13
+ @Controller('<%= fileName %>s')
14
+ export class <%= className %>Controller {
15
+ constructor(private readonly service: <%= className %>Service) {}
16
+
17
+ @Get()
18
+ findAll(@Query() query: FindManyOptions<<%= className %>Model>) {
19
+ return this.service.findAll(query);
20
+ }
21
+
22
+ @Get('by-ids')
23
+ findByIds(@Query('ids') ids: string) {
24
+ return this.service.findByIds(ids.split(','));
25
+ }
26
+
27
+ @Get(':id')
28
+ findById(@Param('id', ParseUUIDPipe) id: string) {
29
+ return this.service.findById(id);
30
+ }
31
+
32
+ @Delete(':id')
33
+ remove(@Param('id', ParseUUIDPipe) id: string) {
34
+ return this.service.remove(id);
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import { Column, Entity } from 'typeorm';
2
+ import { BaseEntity } from '../../common/entities/base.entity';
3
+
4
+ @Entity('<%= fileName %>s')
5
+ export class <%= className %>Model extends BaseEntity {
6
+ // @Column()
7
+ // name: string;
8
+ }
@@ -0,0 +1,12 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule } from '@nestjs/typeorm';
3
+ import { <%= className %>Model } from './<%= fileName %>.model';
4
+ import { <%= className %>Repository } from './<%= fileName %>.repository';
5
+ import { <%= className %>Service } from './<%= fileName %>.service';
6
+
7
+ @Module({
8
+ imports: [TypeOrmModule.forFeature([<%= className %>Model])],
9
+ providers: [<%= className %>Service, <%= className %>Repository],
10
+ exports: [<%= className %>Service, <%= className %>Repository],
11
+ })
12
+ export class <%= className %>Module {}
@@ -0,0 +1,15 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { InjectRepository } from '@nestjs/typeorm';
3
+ import { Repository } from 'typeorm';
4
+ import { EntityRepository } from '../../common/repositories/entity.repository';
5
+ import { <%= className %>Model } from './<%= fileName %>.model';
6
+
7
+ @Injectable()
8
+ export class <%= className %>Repository extends EntityRepository<<%= className %>Model> {
9
+ constructor(
10
+ @InjectRepository(<%= className %>Model)
11
+ repo: Repository<<%= className %>Model>,
12
+ ) {
13
+ super(repo);
14
+ }
15
+ }
@@ -0,0 +1,11 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { EntityService } from '../../common/services/entity.service';
3
+ import { <%= className %>Model } from './<%= fileName %>.model';
4
+ import { <%= className %>Repository } from './<%= fileName %>.repository';
5
+
6
+ @Injectable()
7
+ export class <%= className %>Service extends EntityService<<%= className %>Model> {
8
+ constructor(repository: <%= className %>Repository) {
9
+ super(repository);
10
+ }
11
+ }
@@ -0,0 +1,31 @@
1
+ import { Test } from '@nestjs/testing';
2
+ import { getRepositoryToken } from '@nestjs/typeorm';
3
+ import { Repository } from 'typeorm';
4
+ import { <%= className %>Model } from '../<%= fileName %>.model';
5
+ import { <%= className %>Module } from '../<%= fileName %>.module';
6
+ import { <%= className %>Service } from '../<%= fileName %>.service';
7
+
8
+ describe('<%= className %>Service', () => {
9
+ let service: <%= className %>Service;
10
+
11
+ beforeAll(async () => {
12
+ const module = await Test.createTestingModule({
13
+ imports: [<%= className %>Module],
14
+ })
15
+ .overrideProvider(getRepositoryToken(<%= className %>Model))
16
+ .useValue({
17
+ find: jest.fn(),
18
+ findOne: jest.fn(),
19
+ findOneBy: jest.fn(),
20
+ save: jest.fn(),
21
+ delete: jest.fn(),
22
+ } satisfies Partial<Repository<<%= className %>Model>>)
23
+ .compile();
24
+
25
+ service = module.get(<%= className %>Service);
26
+ });
27
+
28
+ it('is defined', () => {
29
+ expect(service).toBeDefined();
30
+ });
31
+ });
@@ -0,0 +1,44 @@
1
+ const { formatFiles, generateFiles, names } = require('@nx/devkit');
2
+ const { execSync } = require('node:child_process');
3
+ const { join } = require('node:path');
4
+
5
+ module.exports = async function (tree, options) {
6
+ const n = names(options.name);
7
+ const modulePath = `apps/backend/src/modules/${n.fileName}`;
8
+
9
+ generateFiles(
10
+ tree,
11
+ join(__dirname, 'files'),
12
+ modulePath,
13
+ { ...n, tmpl: '' },
14
+ );
15
+
16
+ if (options.crud) {
17
+ generateFiles(
18
+ tree,
19
+ join(__dirname, 'crud-files'),
20
+ `apps/backend/src/api/${n.fileName}`,
21
+ { ...n, tmpl: '' },
22
+ );
23
+ }
24
+
25
+ await formatFiles(tree);
26
+
27
+ console.log(`\n✓ Module created at ${modulePath}/`);
28
+ console.log(` → Import ${n.className}Module in app.module.ts\n`);
29
+
30
+ if (options.crud) {
31
+ console.log(`✓ Controller created at apps/backend/src/api/${n.fileName}/`);
32
+ console.log(` → Add ${n.className}Controller to a module and import ${n.className}Module there\n`);
33
+ }
34
+
35
+ return () => {
36
+ const targets = [modulePath];
37
+ if (options.crud) targets.push(`apps/backend/src/api/${n.fileName}`);
38
+
39
+ execSync(`npx prettier --write ${targets.join(' ')}`, {
40
+ cwd: tree.root,
41
+ stdio: 'inherit',
42
+ });
43
+ };
44
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "cli": "nx",
4
+ "id": "module",
5
+ "title": "Create a NestJS module",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Module name, singular lowercase (e.g. trainer)",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ }
15
+ },
16
+ "crud": {
17
+ "type": "boolean",
18
+ "description": "Generate a CRUD controller in src/api/<name>/",
19
+ "default": false,
20
+ "x-prompt": "Generate a CRUD controller?"
21
+ }
22
+ },
23
+ "required": ["name"]
24
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@local/generators",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "generators": "./generators.json"
6
+ }
@@ -14,10 +14,13 @@
14
14
  "react-dom": "19.2.4"
15
15
  },
16
16
  "devDependencies": {
17
+ "@tailwindcss/postcss": "^4.0.0",
17
18
  "@types/node": "^20",
18
19
  "@types/react": "^19",
19
20
  "@types/react-dom": "^19",
20
21
  "babel-plugin-react-compiler": "1.0.0",
22
+ "postcss": "^8.0.0",
23
+ "tailwindcss": "^4.0.0",
21
24
  "typescript": "^5"
22
25
  }
23
26
  }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,39 @@
1
+ import { useState } from "react";
2
+
3
+ type UseTemplateComponent = {
4
+ state: {
5
+ count: number;
6
+ };
7
+ handlers: {
8
+ handleCountIncrement: () => void;
9
+ handleCountDecrement: () => void;
10
+ handleCountReset: () => void;
11
+ };
12
+ };
13
+
14
+ export const useTemplateComponent = (): UseTemplateComponent => {
15
+ const [count, setCount] = useState<number>(0);
16
+
17
+ const handleCountIncrement = () => {
18
+ setCount((prev) => prev + 1);
19
+ };
20
+
21
+ const handleCountDecrement = () => {
22
+ setCount((prev) => prev - 1);
23
+ };
24
+
25
+ const handleCountReset = () => {
26
+ setCount(0);
27
+ };
28
+
29
+ return {
30
+ state: {
31
+ count,
32
+ },
33
+ handlers: {
34
+ handleCountIncrement,
35
+ handleCountDecrement,
36
+ handleCountReset,
37
+ },
38
+ };
39
+ };
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { useTemplateComponent } from "./TemplateComponent.behaviour";
3
+
4
+ export const TemplateComponent = () => {
5
+ const { state, handlers } = useTemplateComponent();
6
+ return (
7
+ <div>
8
+ <div>Count: {state.count}</div>
9
+ <button onClick={handlers.handleCountIncrement}>Increment</button>
10
+ <button onClick={handlers.handleCountDecrement}>Decrement</button>
11
+ <button onClick={handlers.handleCountReset}>Reset</button>
12
+ </div>
13
+ );
14
+ };
@@ -25,6 +25,8 @@
25
25
  "globals": "^17.4.0",
26
26
  "typescript": "~5.9.3",
27
27
  "typescript-eslint": "^8.57.0",
28
+ "@tailwindcss/vite": "^4.0.0",
29
+ "tailwindcss": "^4.0.0",
28
30
  "vite": "^8.0.1"
29
31
  }
30
32
  }
@@ -0,0 +1,39 @@
1
+ import { useState } from "react";
2
+
3
+ type UseTemplateComponent = {
4
+ state: {
5
+ count: number;
6
+ };
7
+ handlers: {
8
+ handleCountIncrement: () => void;
9
+ handleCountDecrement: () => void;
10
+ handleCountReset: () => void;
11
+ };
12
+ };
13
+
14
+ export const useTemplateComponent = (): UseTemplateComponent => {
15
+ const [count, setCount] = useState<number>(0);
16
+
17
+ const handleCountIncrement = () => {
18
+ setCount((prev) => prev + 1);
19
+ };
20
+
21
+ const handleCountDecrement = () => {
22
+ setCount((prev) => prev - 1);
23
+ };
24
+
25
+ const handleCountReset = () => {
26
+ setCount(0);
27
+ };
28
+
29
+ return {
30
+ state: {
31
+ count,
32
+ },
33
+ handlers: {
34
+ handleCountIncrement,
35
+ handleCountDecrement,
36
+ handleCountReset,
37
+ },
38
+ };
39
+ };
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { useTemplateComponent } from "./TemplateComponent.behaviour";
3
+
4
+ export const TemplateComponent = () => {
5
+ const { state, handlers } = useTemplateComponent();
6
+ return (
7
+ <div>
8
+ <div>Count: {state.count}</div>
9
+ <button onClick={handlers.handleCountIncrement}>Increment</button>
10
+ <button onClick={handlers.handleCountDecrement}>Decrement</button>
11
+ <button onClick={handlers.handleCountReset}>Reset</button>
12
+ </div>
13
+ );
14
+ };
@@ -1,8 +1 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- }
4
-
5
- body {
6
- margin: 0;
7
- font-family: system-ui, sans-serif;
8
- }
1
+ @import "tailwindcss";
@@ -1,7 +1,7 @@
1
- import { defineConfig } from 'vite'
1
+ import tailwindcss from '@tailwindcss/vite'
2
2
  import react from '@vitejs/plugin-react'
3
+ import { defineConfig } from 'vite'
3
4
 
4
- // https://vite.dev/config/
5
5
  export default defineConfig({
6
- plugins: [react()],
6
+ plugins: [react(), tailwindcss()],
7
7
  })
@@ -1,22 +0,0 @@
1
- import { Test, TestingModule } from '@nestjs/testing';
2
- import { AppController } from './app.controller';
3
- import { AppService } from './app.service';
4
-
5
- describe('AppController', () => {
6
- let appController: AppController;
7
-
8
- beforeEach(async () => {
9
- const app: TestingModule = await Test.createTestingModule({
10
- controllers: [AppController],
11
- providers: [AppService],
12
- }).compile();
13
-
14
- appController = app.get<AppController>(AppController);
15
- });
16
-
17
- describe('root', () => {
18
- it('should return "Hello World!"', () => {
19
- expect(appController.getHello()).toBe('Hello World!');
20
- });
21
- });
22
- });
@@ -1,12 +0,0 @@
1
- import { Controller, Get } from '@nestjs/common';
2
- import { AppService } from './app.service';
3
-
4
- @Controller()
5
- export class AppController {
6
- constructor(private readonly appService: AppService) {}
7
-
8
- @Get()
9
- getHello(): string {
10
- return this.appService.getHello();
11
- }
12
- }
@@ -1,8 +0,0 @@
1
- import { Injectable } from '@nestjs/common';
2
-
3
- @Injectable()
4
- export class AppService {
5
- getHello(): string {
6
- return 'Hello World!';
7
- }
8
- }
@@ -1,10 +0,0 @@
1
- *, *::before, *::after {
2
- box-sizing: border-box;
3
- padding: 0;
4
- margin: 0;
5
- }
6
-
7
- body {
8
- font-family: system-ui, sans-serif;
9
- -webkit-font-smoothing: antialiased;
10
- }
File without changes