@geekmidas/cli 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{config-BYn5yUt5.cjs → config-6JHOwLCx.cjs} +30 -2
  3. package/dist/{config-dLNQIvDR.mjs.map → config-6JHOwLCx.cjs.map} +1 -1
  4. package/dist/{config-dLNQIvDR.mjs → config-DxASSNjr.mjs} +25 -3
  5. package/dist/{config-BYn5yUt5.cjs.map → config-DxASSNjr.mjs.map} +1 -1
  6. package/dist/config.cjs +3 -2
  7. package/dist/config.d.cts +14 -2
  8. package/dist/config.d.cts.map +1 -1
  9. package/dist/config.d.mts +14 -2
  10. package/dist/config.d.mts.map +1 -1
  11. package/dist/config.mjs +3 -3
  12. package/dist/{index-Bj5VNxEL.d.mts → index-C-KxSGGK.d.mts} +2 -2
  13. package/dist/{index-Ba21_lNt.d.cts.map → index-C-KxSGGK.d.mts.map} +1 -1
  14. package/dist/{index-Ba21_lNt.d.cts → index-Cyk2rTyj.d.cts} +2 -2
  15. package/dist/{index-Bj5VNxEL.d.mts.map → index-Cyk2rTyj.d.cts.map} +1 -1
  16. package/dist/index.cjs +555 -139
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +519 -103
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/{openapi-CMTyaIJJ.mjs → openapi-BYlyAbH3.mjs} +6 -5
  21. package/dist/openapi-BYlyAbH3.mjs.map +1 -0
  22. package/dist/{openapi-CqblwJZ4.cjs → openapi-CnvwSRDU.cjs} +6 -5
  23. package/dist/openapi-CnvwSRDU.cjs.map +1 -0
  24. package/dist/openapi.cjs +3 -3
  25. package/dist/openapi.d.cts +1 -0
  26. package/dist/openapi.d.cts.map +1 -1
  27. package/dist/openapi.d.mts +1 -0
  28. package/dist/openapi.d.mts.map +1 -1
  29. package/dist/openapi.mjs +3 -3
  30. package/dist/workspace/index.cjs +1 -1
  31. package/dist/workspace/index.d.cts +1 -1
  32. package/dist/workspace/index.d.mts +1 -1
  33. package/dist/workspace/index.mjs +1 -1
  34. package/dist/{workspace-Dy8k7Wru.mjs → workspace-9IQIjwkQ.mjs} +5 -3
  35. package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
  36. package/dist/{workspace-DIMnYaYt.cjs → workspace-D2ocAlpl.cjs} +5 -3
  37. package/dist/workspace-D2ocAlpl.cjs.map +1 -0
  38. package/package.json +11 -10
  39. package/src/config.ts +44 -0
  40. package/src/dev/__tests__/index.spec.ts +490 -0
  41. package/src/dev/index.ts +313 -18
  42. package/src/generators/Generator.ts +4 -1
  43. package/src/init/__tests__/generators.spec.ts +167 -18
  44. package/src/init/__tests__/init.spec.ts +66 -3
  45. package/src/init/generators/auth.ts +6 -5
  46. package/src/init/generators/config.ts +49 -7
  47. package/src/init/generators/docker.ts +8 -8
  48. package/src/init/generators/index.ts +1 -0
  49. package/src/init/generators/models.ts +3 -5
  50. package/src/init/generators/package.ts +4 -0
  51. package/src/init/generators/test.ts +133 -0
  52. package/src/init/generators/ui.ts +13 -12
  53. package/src/init/generators/web.ts +9 -8
  54. package/src/init/index.ts +2 -0
  55. package/src/init/templates/api.ts +6 -6
  56. package/src/init/templates/minimal.ts +2 -2
  57. package/src/init/templates/worker.ts +2 -2
  58. package/src/init/versions.ts +4 -4
  59. package/src/openapi.ts +6 -2
  60. package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
  61. package/src/test/__tests__/api.spec.ts +199 -0
  62. package/src/test/__tests__/auth.spec.ts +162 -0
  63. package/src/test/__tests__/index.spec.ts +323 -0
  64. package/src/test/__tests__/web.spec.ts +210 -0
  65. package/src/test/index.ts +165 -14
  66. package/src/workspace/__tests__/index.spec.ts +3 -0
  67. package/src/workspace/index.ts +4 -2
  68. package/dist/openapi-CMTyaIJJ.mjs.map +0 -1
  69. package/dist/openapi-CqblwJZ4.cjs.map +0 -1
  70. package/dist/workspace-DIMnYaYt.cjs.map +0 -1
  71. package/dist/workspace-Dy8k7Wru.mjs.map +0 -1
@@ -417,6 +417,7 @@ describe('initCommand', () => {
417
417
  expect(pkg.dependencies.next).toBeDefined();
418
418
  expect(pkg.dependencies.react).toBeDefined();
419
419
  expect(pkg.scripts.dev).toContain('next dev');
420
+ expect(pkg.scripts.dev).toContain('-p $PORT');
420
421
  });
421
422
 
422
423
  it('should include services config in workspace', async () => {
@@ -472,6 +473,68 @@ describe('initCommand', () => {
472
473
  });
473
474
  });
474
475
 
476
+ describe('test infrastructure', () => {
477
+ it('should generate test files for standalone app with database', async () => {
478
+ await initCommand('my-api', {
479
+ template: 'api',
480
+ yes: true,
481
+ skipInstall: true,
482
+ });
483
+
484
+ const projectDir = join(tempDir, 'my-api');
485
+ expect(existsSync(join(projectDir, 'test/config.ts'))).toBe(true);
486
+ expect(existsSync(join(projectDir, 'test/globalSetup.ts'))).toBe(true);
487
+ expect(existsSync(join(projectDir, 'test/factory/index.ts'))).toBe(true);
488
+ expect(existsSync(join(projectDir, 'test/factory/users.ts'))).toBe(true);
489
+ expect(existsSync(join(projectDir, 'test/example.spec.ts'))).toBe(true);
490
+ expect(existsSync(join(projectDir, 'vitest.config.ts'))).toBe(true);
491
+ });
492
+
493
+ it('should include testkit and faker in devDependencies', async () => {
494
+ await initCommand('my-api', {
495
+ template: 'api',
496
+ yes: true,
497
+ skipInstall: true,
498
+ });
499
+
500
+ const pkgPath = join(tempDir, 'my-api', 'package.json');
501
+ const content = await readFile(pkgPath, 'utf-8');
502
+ const pkg = JSON.parse(content);
503
+ expect(pkg.devDependencies['@geekmidas/testkit']).toMatch(/^~/);
504
+ expect(pkg.devDependencies['@faker-js/faker']).toMatch(/^~/);
505
+ });
506
+
507
+ it('should generate test files for fullstack api app', async () => {
508
+ await initCommand('my-fullstack', {
509
+ template: 'fullstack',
510
+ yes: true,
511
+ skipInstall: true,
512
+ });
513
+
514
+ const apiDir = join(tempDir, 'my-fullstack', 'apps/api');
515
+ expect(existsSync(join(apiDir, 'test/config.ts'))).toBe(true);
516
+ expect(existsSync(join(apiDir, 'test/globalSetup.ts'))).toBe(true);
517
+ expect(existsSync(join(apiDir, 'test/factory/index.ts'))).toBe(true);
518
+ expect(existsSync(join(apiDir, 'test/factory/users.ts'))).toBe(true);
519
+ expect(existsSync(join(apiDir, 'vitest.config.ts'))).toBe(true);
520
+ });
521
+
522
+ it('should include globalSetup and vite-tsconfig-paths in vitest.config.ts', async () => {
523
+ await initCommand('my-api', {
524
+ template: 'api',
525
+ yes: true,
526
+ skipInstall: true,
527
+ });
528
+
529
+ const vitestConfigPath = join(tempDir, 'my-api', 'vitest.config.ts');
530
+ const content = await readFile(vitestConfigPath, 'utf-8');
531
+ expect(content).toContain('globalSetup');
532
+ expect(content).toContain('./test/globalSetup.ts');
533
+ expect(content).toContain('vite-tsconfig-paths');
534
+ expect(content).not.toContain('globals: true');
535
+ });
536
+ });
537
+
475
538
  describe('docker-compose', () => {
476
539
  it('should include postgres for database-enabled projects', async () => {
477
540
  await initCommand('my-api', {
@@ -483,7 +546,7 @@ describe('initCommand', () => {
483
546
  const dockerPath = join(tempDir, 'my-api', 'docker-compose.yml');
484
547
  const content = await readFile(dockerPath, 'utf-8');
485
548
  expect(content).toContain('postgres:16-alpine');
486
- expect(content).toContain('5432:5432');
549
+ expect(content).toContain("'${POSTGRES_HOST_PORT:-5432}:5432'");
487
550
  });
488
551
 
489
552
  it('should include serverless-redis-http for serverless template', async () => {
@@ -508,8 +571,8 @@ describe('initCommand', () => {
508
571
  const dockerPath = join(tempDir, 'my-api', 'docker-compose.yml');
509
572
  const content = await readFile(dockerPath, 'utf-8');
510
573
  expect(content).toContain('rabbitmq:3-management-alpine');
511
- expect(content).toContain('5672:5672');
512
- expect(content).toContain('15672:15672');
574
+ expect(content).toContain("'${RABBITMQ_HOST_PORT:-5672}:5672'");
575
+ expect(content).toContain("'${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'");
513
576
  });
514
577
  });
515
578
  });
@@ -53,6 +53,7 @@ export function generateAuthAppFiles(
53
53
  extends: '../../tsconfig.json',
54
54
  compilerOptions: {
55
55
  noEmit: true,
56
+ allowImportingTsExtensions: true,
56
57
  baseUrl: '.',
57
58
  paths: {
58
59
  '~/*': ['./src/*'],
@@ -89,8 +90,8 @@ export const logger = createLogger();
89
90
  const authTs = `import { betterAuth } from 'better-auth';
90
91
  import { magicLink } from 'better-auth/plugins';
91
92
  import pg from 'pg';
92
- import { envParser } from './config/env.js';
93
- import { logger } from './config/logger.js';
93
+ import { envParser } from './config/env.ts';
94
+ import { logger } from './config/logger.ts';
94
95
 
95
96
  // Parse auth-specific config (no defaults - values from secrets)
96
97
  const authConfig = envParser
@@ -135,9 +136,9 @@ export type Auth = typeof auth;
135
136
  const indexTs = `import { Hono } from 'hono';
136
137
  import { cors } from 'hono/cors';
137
138
  import { serve } from '@hono/node-server';
138
- import { auth } from './auth.js';
139
- import { envParser } from './config/env.js';
140
- import { logger } from './config/logger.js';
139
+ import { auth } from './auth.ts';
140
+ import { envParser } from './config/env.ts';
141
+ import { logger } from './config/logger.ts';
141
142
 
142
143
  // Parse server config (no defaults - values from secrets)
143
144
  const serverConfig = envParser
@@ -4,6 +4,21 @@ import type {
4
4
  TemplateOptions,
5
5
  } from '../templates/index.js';
6
6
 
7
+ /**
8
+ * Vitest config content with globalSetup for database-enabled apps
9
+ */
10
+ const vitestConfigContent = `import { defineConfig } from 'vitest/config';
11
+ import tsconfigPaths from 'vite-tsconfig-paths';
12
+
13
+ export default defineConfig({
14
+ plugins: [tsconfigPaths()],
15
+ test: {
16
+ environment: 'node',
17
+ globalSetup: './test/globalSetup.ts',
18
+ },
19
+ });
20
+ `;
21
+
7
22
  /**
8
23
  * Generate configuration files (gkm.config.ts, tsconfig.json, biome.json, turbo.json)
9
24
  */
@@ -91,6 +106,7 @@ export default defineConfig({
91
106
  extends: '../../tsconfig.json',
92
107
  compilerOptions: {
93
108
  noEmit: true,
109
+ allowImportingTsExtensions: true,
94
110
  baseUrl: '.',
95
111
  paths: {
96
112
  '~/*': ['./src/*'],
@@ -111,10 +127,8 @@ export default defineConfig({
111
127
  skipLibCheck: true,
112
128
  forceConsistentCasingInFileNames: true,
113
129
  resolveJsonModule: true,
114
- declaration: true,
115
- declarationMap: true,
116
- outDir: './dist',
117
- rootDir: './src',
130
+ noEmit: true,
131
+ allowImportingTsExtensions: true,
118
132
  },
119
133
  include: ['src/**/*.ts'],
120
134
  exclude: ['node_modules', 'dist'],
@@ -122,7 +136,7 @@ export default defineConfig({
122
136
 
123
137
  // Skip biome.json and turbo.json for monorepo (they're at root)
124
138
  if (options.monorepo) {
125
- return [
139
+ const files: GeneratedFile[] = [
126
140
  {
127
141
  path: 'gkm.config.ts',
128
142
  content: gkmConfig,
@@ -132,6 +146,15 @@ export default defineConfig({
132
146
  content: `${JSON.stringify(tsConfig, null, 2)}\n`,
133
147
  },
134
148
  ];
149
+
150
+ if (options.database) {
151
+ files.push({
152
+ path: 'vitest.config.ts',
153
+ content: vitestConfigContent,
154
+ });
155
+ }
156
+
157
+ return files;
135
158
  }
136
159
 
137
160
  // Build biome.json
@@ -210,7 +233,7 @@ export default defineConfig({
210
233
  },
211
234
  };
212
235
 
213
- return [
236
+ const files: GeneratedFile[] = [
214
237
  {
215
238
  path: 'gkm.config.ts',
216
239
  content: gkmConfig,
@@ -228,6 +251,15 @@ export default defineConfig({
228
251
  content: `${JSON.stringify(turboConfig, null, 2)}\n`,
229
252
  },
230
253
  ];
254
+
255
+ if (options.database) {
256
+ files.push({
257
+ path: 'vitest.config.ts',
258
+ content: vitestConfigContent,
259
+ });
260
+ }
261
+
262
+ return files;
231
263
  }
232
264
 
233
265
  /**
@@ -255,6 +287,7 @@ function generateSingleAppConfigFiles(
255
287
  extends: '../../tsconfig.json',
256
288
  compilerOptions: {
257
289
  noEmit: true,
290
+ allowImportingTsExtensions: true,
258
291
  baseUrl: '.',
259
292
  paths: {
260
293
  '~/*': ['./src/*'],
@@ -265,10 +298,19 @@ function generateSingleAppConfigFiles(
265
298
  exclude: ['node_modules', 'dist'],
266
299
  };
267
300
 
268
- return [
301
+ const files: GeneratedFile[] = [
269
302
  {
270
303
  path: 'tsconfig.json',
271
304
  content: `${JSON.stringify(tsConfig, null, 2)}\n`,
272
305
  },
273
306
  ];
307
+
308
+ if (options.database) {
309
+ files.push({
310
+ path: 'vitest.config.ts',
311
+ content: vitestConfigContent,
312
+ });
313
+ }
314
+
315
+ return files;
274
316
  }
@@ -50,7 +50,7 @@ export function generateDockerFiles(
50
50
  POSTGRES_PASSWORD: postgres
51
51
  POSTGRES_DB: ${options.name.replace(/-/g, '_')}_dev
52
52
  ports:
53
- - '5432:5432'
53
+ - '\${POSTGRES_HOST_PORT:-5432}:5432'
54
54
  volumes:
55
55
  - postgres_data:/var/lib/postgresql/data${initVolume}
56
56
  healthcheck:
@@ -83,7 +83,7 @@ export function generateDockerFiles(
83
83
  container_name: ${options.name}-redis
84
84
  restart: unless-stopped
85
85
  ports:
86
- - '6379:6379'
86
+ - '\${REDIS_HOST_PORT:-6379}:6379'
87
87
  volumes:
88
88
  - redis_data:/data
89
89
  healthcheck:
@@ -97,7 +97,7 @@ export function generateDockerFiles(
97
97
  container_name: ${options.name}-serverless-redis
98
98
  restart: unless-stopped
99
99
  ports:
100
- - '8079:80'
100
+ - '\${SRH_HOST_PORT:-8079}:80'
101
101
  environment:
102
102
  SRH_MODE: env
103
103
  SRH_TOKEN: local_dev_token
@@ -113,7 +113,7 @@ export function generateDockerFiles(
113
113
  container_name: ${options.name}-redis
114
114
  restart: unless-stopped
115
115
  ports:
116
- - '6379:6379'
116
+ - '\${REDIS_HOST_PORT:-6379}:6379'
117
117
  volumes:
118
118
  - redis_data:/data
119
119
  healthcheck:
@@ -131,8 +131,8 @@ export function generateDockerFiles(
131
131
  container_name: ${options.name}-rabbitmq
132
132
  restart: unless-stopped
133
133
  ports:
134
- - '5672:5672'
135
- - '15672:15672'
134
+ - '\${RABBITMQ_HOST_PORT:-5672}:5672'
135
+ - '\${RABBITMQ_MGMT_HOST_PORT:-15672}:15672'
136
136
  environment:
137
137
  RABBITMQ_DEFAULT_USER: guest
138
138
  RABBITMQ_DEFAULT_PASS: guest
@@ -153,8 +153,8 @@ export function generateDockerFiles(
153
153
  container_name: ${options.name}-mailpit
154
154
  restart: unless-stopped
155
155
  ports:
156
- - '1025:1025'
157
- - '8025:8025'
156
+ - '\${MAILPIT_SMTP_HOST_PORT:-1025}:1025'
157
+ - '\${MAILPIT_UI_HOST_PORT:-8025}:8025'
158
158
  environment:
159
159
  MP_SMTP_AUTH_ACCEPT_ANY: 1
160
160
  MP_SMTP_AUTH_ALLOW_INSECURE: 1`);
@@ -3,3 +3,4 @@ export { generateConfigFiles } from './config.js';
3
3
  export { generateEnvFiles } from './env.js';
4
4
  export { generatePackageJson } from './package.js';
5
5
  export { generateSourceFiles } from './source.js';
6
+ export { generateTestFiles } from './test.js';
@@ -35,10 +35,8 @@ export function generateModelsPackage(
35
35
  const tsConfig = {
36
36
  extends: '../../tsconfig.json',
37
37
  compilerOptions: {
38
- declaration: true,
39
- declarationMap: true,
40
- outDir: './dist',
41
- rootDir: './src',
38
+ noEmit: true,
39
+ allowImportingTsExtensions: true,
42
40
  },
43
41
  include: ['src/**/*.ts'],
44
42
  exclude: ['node_modules', 'dist'],
@@ -88,7 +86,7 @@ export type Pagination = z.infer<typeof PaginationSchema>;
88
86
 
89
87
  // user.ts - user-related schemas
90
88
  const userTs = `import { z } from 'zod';
91
- import { IdSchema, TimestampsSchema } from './common.js';
89
+ import { IdSchema, TimestampsSchema } from './common.ts';
92
90
 
93
91
  // ============================================
94
92
  // User Schemas
@@ -35,6 +35,10 @@ export function generatePackageJson(
35
35
  dependencies.kysely = '~0.28.2';
36
36
  dependencies.pg = '~8.16.0';
37
37
  devDependencies['@types/pg'] = '~8.15.0';
38
+ devDependencies['@geekmidas/testkit'] =
39
+ GEEKMIDAS_VERSIONS['@geekmidas/testkit'];
40
+ devDependencies['@faker-js/faker'] = '~9.8.0';
41
+ devDependencies['vite-tsconfig-paths'] = '~5.1.0';
38
42
  }
39
43
 
40
44
  // For monorepo apps, remove biome/turbo/esbuild (they're at root) and lint/fmt scripts
@@ -0,0 +1,133 @@
1
+ import type {
2
+ GeneratedFile,
3
+ TemplateConfig,
4
+ TemplateOptions,
5
+ } from '../templates/index.js';
6
+
7
+ /**
8
+ * Generate test infrastructure files when database is enabled.
9
+ * Includes transaction-isolated test config, global setup with migrations,
10
+ * factory system with builders/seeds, and an example spec.
11
+ */
12
+ export function generateTestFiles(
13
+ options: TemplateOptions,
14
+ _template: TemplateConfig,
15
+ ): GeneratedFile[] {
16
+ if (!options.database) {
17
+ return [];
18
+ }
19
+
20
+ return [
21
+ // test/config.ts - Wraps vitest `it` with transaction auto-rollback
22
+ {
23
+ path: 'test/config.ts',
24
+ content: `import { it as itVitest } from 'vitest';
25
+ import { Kysely, PostgresDialect } from 'kysely';
26
+ import pg from 'pg';
27
+ import { wrapVitestKyselyTransaction } from '@geekmidas/testkit/kysely';
28
+ import type { Database } from '~/services/database.ts';
29
+
30
+ const connection = new Kysely<Database>({
31
+ dialect: new PostgresDialect({
32
+ pool: new pg.Pool({ connectionString: process.env.DATABASE_URL }),
33
+ }),
34
+ });
35
+
36
+ export const it = wrapVitestKyselyTransaction<Database>(itVitest, {
37
+ connection,
38
+ });
39
+ `,
40
+ },
41
+
42
+ // test/globalSetup.ts - Runs migrations on the test database
43
+ // Note: gkm test automatically rewrites DATABASE_URL to use a _test
44
+ // suffixed database and creates it if needed. This setup only runs
45
+ // migrations.
46
+ {
47
+ path: 'test/globalSetup.ts',
48
+ content: `import { Kysely, PostgresDialect } from 'kysely';
49
+ import pg from 'pg';
50
+ import { PostgresKyselyMigrator } from '@geekmidas/testkit/kysely';
51
+ import type { Database } from '~/services/database.ts';
52
+
53
+ export async function setup() {
54
+ const testUrl = process.env.DATABASE_URL;
55
+ if (!testUrl) throw new Error('DATABASE_URL is required for tests');
56
+
57
+ // Run migrations on the test database
58
+ // (gkm test already rewrites DATABASE_URL to point to the _test database)
59
+ const db = new Kysely<Database>({
60
+ dialect: new PostgresDialect({
61
+ pool: new pg.Pool({ connectionString: testUrl }),
62
+ }),
63
+ });
64
+
65
+ const migrator = new PostgresKyselyMigrator({
66
+ db,
67
+ migrationsPath: './src/migrations',
68
+ });
69
+
70
+ await migrator.migrateToLatest();
71
+ await db.destroy();
72
+ }
73
+ `,
74
+ },
75
+
76
+ // test/factory/index.ts - Factory aggregator
77
+ {
78
+ path: 'test/factory/index.ts',
79
+ content: `import type { Kysely } from 'kysely';
80
+ import { KyselyFactory } from '@geekmidas/testkit/kysely';
81
+ import type { Database } from '~/services/database.ts';
82
+ import { usersBuilder } from './users.ts';
83
+
84
+ const builders = { users: usersBuilder };
85
+ const seeds = {};
86
+
87
+ export function createFactory(db: Kysely<Database>) {
88
+ return new KyselyFactory<Database, typeof builders, typeof seeds>(
89
+ builders,
90
+ seeds,
91
+ db,
92
+ );
93
+ }
94
+
95
+ export type Factory = ReturnType<typeof createFactory>;
96
+ `,
97
+ },
98
+
99
+ // test/factory/users.ts - Example builder
100
+ {
101
+ path: 'test/factory/users.ts',
102
+ content: `import { KyselyFactory } from '@geekmidas/testkit/kysely';
103
+ import type { Database } from '~/services/database.ts';
104
+
105
+ export const usersBuilder = KyselyFactory.createBuilder<Database, 'users'>(
106
+ 'users',
107
+ ({ faker }) => ({
108
+ id: faker.string.uuid(),
109
+ name: faker.person.fullName(),
110
+ email: faker.internet.email(),
111
+ created_at: new Date(),
112
+ }),
113
+ );
114
+ `,
115
+ },
116
+
117
+ // test/example.spec.ts - Example test showing usage
118
+ {
119
+ path: 'test/example.spec.ts',
120
+ content: `import { describe, expect } from 'vitest';
121
+ import { it } from './config.ts';
122
+
123
+ describe('example', () => {
124
+ it('should have a working test setup', async ({ db }) => {
125
+ // db is a transaction-wrapped Kysely instance
126
+ // All changes are automatically rolled back after the test
127
+ expect(db).toBeDefined();
128
+ });
129
+ });
130
+ `,
131
+ },
132
+ ];
133
+ }
@@ -72,6 +72,7 @@ export function generateUiPackageFiles(
72
72
  jsx: 'react-jsx',
73
73
  lib: ['ES2023', 'DOM', 'DOM.Iterable'],
74
74
  noEmit: true,
75
+ allowImportingTsExtensions: true,
75
76
  baseUrl: '.',
76
77
  paths: {
77
78
  '~/*': ['./src/*'],
@@ -1291,8 +1292,8 @@ export const Alert: Story = {
1291
1292
  `;
1292
1293
 
1293
1294
  // src/components/ui/index.ts
1294
- const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button';
1295
- export { Input } from './input';
1295
+ const componentsUiIndex = `export { Button, type ButtonProps, buttonVariants } from './button.tsx';
1296
+ export { Input } from './input.tsx';
1296
1297
  export {
1297
1298
  Card,
1298
1299
  CardHeader,
@@ -1300,17 +1301,17 @@ export {
1300
1301
  CardTitle,
1301
1302
  CardDescription,
1302
1303
  CardContent,
1303
- } from './card';
1304
- export { Label } from './label';
1305
- export { Badge, type BadgeProps, badgeVariants } from './badge';
1306
- export { Separator } from './separator';
1307
- export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
1304
+ } from './card.tsx';
1305
+ export { Label } from './label.tsx';
1306
+ export { Badge, type BadgeProps, badgeVariants } from './badge.tsx';
1307
+ export { Separator } from './separator.tsx';
1308
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs.tsx';
1308
1309
  export {
1309
1310
  Tooltip,
1310
1311
  TooltipTrigger,
1311
1312
  TooltipContent,
1312
1313
  TooltipProvider,
1313
- } from './tooltip';
1314
+ } from './tooltip.tsx';
1314
1315
  export {
1315
1316
  Dialog,
1316
1317
  DialogPortal,
@@ -1322,7 +1323,7 @@ export {
1322
1323
  DialogFooter,
1323
1324
  DialogTitle,
1324
1325
  DialogDescription,
1325
- } from './dialog';
1326
+ } from './dialog.tsx';
1326
1327
  `;
1327
1328
 
1328
1329
  // Rename component files to index.tsx (same content)
@@ -1331,17 +1332,17 @@ export {
1331
1332
  const cardIndexTsx = cardTsx;
1332
1333
 
1333
1334
  // src/components/index.ts
1334
- const componentsIndex = `export * from './ui';
1335
+ const componentsIndex = `export * from './ui/index.ts';
1335
1336
  `;
1336
1337
 
1337
1338
  // src/index.ts
1338
1339
  const indexTs = `// @${options.name}/ui - Shared UI component library
1339
1340
 
1340
1341
  // shadcn/ui components
1341
- export * from './components';
1342
+ export * from './components/index.ts';
1342
1343
 
1343
1344
  // Utilities
1344
- export { cn } from './lib/utils';
1345
+ export { cn } from './lib/utils.ts';
1345
1346
  `;
1346
1347
 
1347
1348
  // .gitignore for UI package
@@ -20,7 +20,7 @@ export function generateWebAppFiles(options: TemplateOptions): GeneratedFile[] {
20
20
  private: true,
21
21
  type: 'module',
22
22
  scripts: {
23
- dev: 'gkm exec -- next dev --turbopack',
23
+ dev: 'gkm exec -- next dev --turbopack -p $PORT',
24
24
  build: 'gkm exec -- next build',
25
25
  start: 'next start',
26
26
  typecheck: 'tsc --noEmit',
@@ -78,6 +78,7 @@ export default nextConfig;
78
78
  skipLibCheck: true,
79
79
  strict: true,
80
80
  noEmit: true,
81
+ allowImportingTsExtensions: true,
81
82
  esModuleInterop: true,
82
83
  module: 'ESNext',
83
84
  moduleResolution: 'bundler',
@@ -169,7 +170,7 @@ export const serverConfig = envParser
169
170
  // Auth client for better-auth
170
171
  const authClientTs = `import { createAuthClient } from 'better-auth/react';
171
172
  import { magicLinkClient } from 'better-auth/client/plugins';
172
- import { clientConfig } from '~/config/client';
173
+ import { clientConfig } from '~/config/client.ts';
173
174
 
174
175
  export const authClient = createAuthClient({
175
176
  baseURL: clientConfig.authUrl,
@@ -183,7 +184,7 @@ export const { signIn, signUp, signOut, useSession, magicLink } = authClient;
183
184
  const providersTsx = `'use client';
184
185
 
185
186
  import { QueryClientProvider } from '@tanstack/react-query';
186
- import { getQueryClient } from '~/lib/query-client';
187
+ import { getQueryClient } from '~/lib/query-client.ts';
187
188
 
188
189
  export function Providers({ children }: { children: React.ReactNode }) {
189
190
  const queryClient = getQueryClient();
@@ -195,9 +196,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
195
196
  `;
196
197
 
197
198
  // API client setup - uses createApi with shared QueryClient
198
- const apiIndexTs = `import { createApi } from './openapi';
199
- import { getQueryClient } from '~/lib/query-client';
200
- import { clientConfig } from '~/config/client';
199
+ const apiIndexTs = `import { createApi } from './api.ts';
200
+ import { getQueryClient } from '~/lib/query-client.ts';
201
+ import { clientConfig } from '~/config/client.ts';
201
202
 
202
203
  export const api = createApi({
203
204
  baseURL: clientConfig.apiUrl,
@@ -211,7 +212,7 @@ export const api = createApi({
211
212
 
212
213
  // App layout
213
214
  const layoutTsx = `import type { Metadata } from 'next';
214
- import { Providers } from './providers';
215
+ import { Providers } from './providers.tsx';
215
216
  import './globals.css';
216
217
 
217
218
  export const metadata: Metadata = {
@@ -235,7 +236,7 @@ export default function RootLayout({
235
236
  `;
236
237
 
237
238
  // Home page with API example using UI components
238
- const pageTsx = `import { api } from '~/api';
239
+ const pageTsx = `import { api } from '~/api/index.ts';
239
240
  import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '${uiPackage}/components';
240
241
 
241
242
  export default async function Home() {
package/src/init/index.ts CHANGED
@@ -17,6 +17,7 @@ import { generateModelsPackage } from './generators/models.js';
17
17
  import { generateMonorepoFiles } from './generators/monorepo.js';
18
18
  import { generatePackageJson } from './generators/package.js';
19
19
  import { generateSourceFiles } from './generators/source.js';
20
+ import { generateTestFiles } from './generators/test.js';
20
21
  import { generateUiPackageFiles } from './generators/ui.js';
21
22
  import { generateWebAppFiles } from './generators/web.js';
22
23
  import {
@@ -269,6 +270,7 @@ export async function initCommand(
269
270
  ...generateConfigFiles(templateOptions, baseTemplate),
270
271
  ...generateEnvFiles(templateOptions, baseTemplate),
271
272
  ...generateSourceFiles(templateOptions, baseTemplate),
273
+ ...generateTestFiles(templateOptions, baseTemplate),
272
274
  ...(isMonorepo
273
275
  ? []
274
276
  : generateDockerFiles(templateOptions, baseTemplate, dbApps)),
@@ -107,7 +107,7 @@ export const config = envParser
107
107
  path: getRoutePath('health.ts'),
108
108
  content: monorepo
109
109
  ? `import { z } from 'zod';
110
- import { publicRouter } from '~/router';
110
+ import { publicRouter } from '~/router.ts';
111
111
 
112
112
  export const healthEndpoint = publicRouter
113
113
  .get('/health')
@@ -261,8 +261,8 @@ export const authService = {
261
261
  path: 'src/router.ts',
262
262
  content: `import { e } from '@geekmidas/constructs/endpoints';
263
263
  import { UnauthorizedError } from '@geekmidas/errors';
264
- import { authService, type Session } from './services/auth.js';
265
- import { logger } from './config/logger.js';
264
+ import { authService, type Session } from './services/auth.ts';
265
+ import { logger } from './config/logger.ts';
266
266
 
267
267
  // Public router - no auth required
268
268
  export const publicRouter = e.logger(logger);
@@ -288,7 +288,7 @@ export const sessionRouter = r.session<Session>(async ({ services, header }) =>
288
288
  files.push({
289
289
  path: getRoutePath('profile.ts'),
290
290
  content: `import { z } from 'zod';
291
- import { sessionRouter } from '~/router';
291
+ import { sessionRouter } from '~/router.ts';
292
292
 
293
293
  export const profileEndpoint = sessionRouter
294
294
  .get('/profile')
@@ -368,8 +368,8 @@ export const telescope = new Telescope({
368
368
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
369
369
  import { Kysely, PostgresDialect } from 'kysely';
370
370
  import pg from 'pg';
371
- import type { Database } from '../services/database.js';
372
- import { envParser } from './env.js';
371
+ import type { Database } from '~/services/database.ts';
372
+ import { envParser } from '~/config/env.ts';
373
373
 
374
374
  // Parse database config for Studio
375
375
  const studioConfig = envParser
@@ -171,8 +171,8 @@ export const telescope = new Telescope({
171
171
  content: `import { Direction, InMemoryMonitoringStorage, Studio } from '@geekmidas/studio';
172
172
  import { Kysely, PostgresDialect } from 'kysely';
173
173
  import pg from 'pg';
174
- import type { Database } from '../services/database.js';
175
- import { envParser } from './env.js';
174
+ import type { Database } from '~/services/database.ts';
175
+ import { envParser } from '~/config/env.ts';
176
176
 
177
177
  // Parse database config for Studio
178
178
  const studioConfig = envParser