@geekmidas/cli 1.5.1 → 1.6.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/CHANGELOG.md +11 -0
- package/dist/{config-BYn5yUt5.cjs → config-6JHOwLCx.cjs} +30 -2
- package/dist/{config-dLNQIvDR.mjs.map → config-6JHOwLCx.cjs.map} +1 -1
- package/dist/{config-dLNQIvDR.mjs → config-DxASSNjr.mjs} +25 -3
- package/dist/{config-BYn5yUt5.cjs.map → config-DxASSNjr.mjs.map} +1 -1
- package/dist/config.cjs +3 -2
- package/dist/config.d.cts +14 -2
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +14 -2
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +3 -3
- package/dist/{index-Bj5VNxEL.d.mts → index-C-KxSGGK.d.mts} +2 -2
- package/dist/{index-Ba21_lNt.d.cts.map → index-C-KxSGGK.d.mts.map} +1 -1
- package/dist/{index-Ba21_lNt.d.cts → index-Cyk2rTyj.d.cts} +2 -2
- package/dist/{index-Bj5VNxEL.d.mts.map → index-Cyk2rTyj.d.cts.map} +1 -1
- package/dist/index.cjs +549 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +513 -97
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CMTyaIJJ.mjs → openapi-BYlyAbH3.mjs} +6 -5
- package/dist/openapi-BYlyAbH3.mjs.map +1 -0
- package/dist/{openapi-CqblwJZ4.cjs → openapi-CnvwSRDU.cjs} +6 -5
- package/dist/openapi-CnvwSRDU.cjs.map +1 -0
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -0
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -0
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +1 -1
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-Dy8k7Wru.mjs → workspace-9IQIjwkQ.mjs} +5 -3
- package/dist/workspace-9IQIjwkQ.mjs.map +1 -0
- package/dist/{workspace-DIMnYaYt.cjs → workspace-D2ocAlpl.cjs} +5 -3
- package/dist/workspace-D2ocAlpl.cjs.map +1 -0
- package/package.json +6 -5
- package/src/config.ts +44 -0
- package/src/dev/__tests__/index.spec.ts +490 -0
- package/src/dev/index.ts +313 -18
- package/src/generators/Generator.ts +4 -1
- package/src/init/__tests__/generators.spec.ts +167 -18
- package/src/init/__tests__/init.spec.ts +66 -3
- package/src/init/generators/auth.ts +6 -5
- package/src/init/generators/config.ts +49 -7
- package/src/init/generators/docker.ts +8 -8
- package/src/init/generators/index.ts +1 -0
- package/src/init/generators/models.ts +3 -5
- package/src/init/generators/package.ts +4 -0
- package/src/init/generators/test.ts +133 -0
- package/src/init/generators/ui.ts +13 -12
- package/src/init/generators/web.ts +9 -8
- package/src/init/index.ts +2 -0
- package/src/init/templates/api.ts +6 -6
- package/src/init/templates/minimal.ts +2 -2
- package/src/init/templates/worker.ts +2 -2
- package/src/init/versions.ts +3 -3
- package/src/openapi.ts +6 -2
- package/src/test/__tests__/__fixtures__/workspace.ts +104 -0
- package/src/test/__tests__/api.spec.ts +199 -0
- package/src/test/__tests__/auth.spec.ts +162 -0
- package/src/test/__tests__/index.spec.ts +323 -0
- package/src/test/__tests__/web.spec.ts +210 -0
- package/src/test/index.ts +165 -14
- package/src/workspace/__tests__/index.spec.ts +3 -0
- package/src/workspace/index.ts +4 -2
- package/dist/openapi-CMTyaIJJ.mjs.map +0 -1
- package/dist/openapi-CqblwJZ4.cjs.map +0 -1
- package/dist/workspace-DIMnYaYt.cjs.map +0 -1
- 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.
|
|
93
|
-
import { logger } from './config/logger.
|
|
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.
|
|
139
|
-
import { envParser } from './config/env.
|
|
140
|
-
import { logger } from './config/logger.
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`);
|
|
@@ -35,10 +35,8 @@ export function generateModelsPackage(
|
|
|
35
35
|
const tsConfig = {
|
|
36
36
|
extends: '../../tsconfig.json',
|
|
37
37
|
compilerOptions: {
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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 './
|
|
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.
|
|
265
|
-
import { logger } from './config/logger.
|
|
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 '
|
|
372
|
-
import { envParser } from '
|
|
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 '
|
|
175
|
-
import { envParser } from '
|
|
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
|