domain-driver 0.0.4 → 0.1.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.
@@ -0,0 +1,122 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists, resolveImport } from '../utils';
3
+
4
+ function renderHook(hookName: string, name: string, feature: string): string {
5
+ const typePath = resolveImport(feature, `types/${name}.types`);
6
+ const svcPath = (op: string) => resolveImport(feature, `services/${op}${name}.service`);
7
+ const schemaPath = (op: string) => resolveImport(feature, `schemas/${op}${name}.schema`);
8
+
9
+ return `'use client';
10
+
11
+ import { useState, useEffect, useCallback } from 'react';
12
+ import { ${name} } from '${typePath}';
13
+ import { List${name}Service } from '${svcPath('List')}';
14
+ import { Show${name}Service } from '${svcPath('Show')}';
15
+ import { Create${name}Service } from '${svcPath('Create')}';
16
+ import { Update${name}Service } from '${svcPath('Update')}';
17
+ import { Delete${name}Service } from '${svcPath('Delete')}';
18
+ import { Create${name} } from '${schemaPath('Create')}';
19
+ import { Update${name} } from '${schemaPath('Update')}';
20
+
21
+ const listService = new List${name}Service();
22
+ const showService = new Show${name}Service();
23
+ const createService = new Create${name}Service();
24
+ const updateService = new Update${name}Service();
25
+ const deleteService = new Delete${name}Service();
26
+
27
+ export function ${hookName}() {
28
+ const [items, setItems] = useState<${name}[]>([]);
29
+ const [selected, setSelected] = useState<${name} | null>(null);
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ const fetchAll = useCallback(async () => {
34
+ setLoading(true);
35
+ setError(null);
36
+ try {
37
+ const data = await listService.handle();
38
+ setItems(data);
39
+ } catch (err: unknown) {
40
+ setError(err instanceof Error ? err.message : 'Failed to fetch');
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ }, []);
45
+
46
+ const fetchOne = useCallback(async (id: string) => {
47
+ setLoading(true);
48
+ setError(null);
49
+ try {
50
+ const data = await showService.handle(id);
51
+ setSelected(data);
52
+ } catch (err: unknown) {
53
+ setError(err instanceof Error ? err.message : 'Failed to fetch');
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ }, []);
58
+
59
+ const create = useCallback(async (data: Create${name}) => {
60
+ setLoading(true);
61
+ setError(null);
62
+ try {
63
+ const created = await createService.handle(data);
64
+ setItems((prev) => [...prev, created]);
65
+ return created;
66
+ } catch (err: unknown) {
67
+ setError(err instanceof Error ? err.message : 'Failed to create');
68
+ return null;
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ }, []);
73
+
74
+ const update = useCallback(async (id: string, data: Update${name}) => {
75
+ setLoading(true);
76
+ setError(null);
77
+ try {
78
+ const updated = await updateService.handle(id, data);
79
+ setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
80
+ return updated;
81
+ } catch (err: unknown) {
82
+ setError(err instanceof Error ? err.message : 'Failed to update');
83
+ return null;
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ }, []);
88
+
89
+ const remove = useCallback(async (id: string) => {
90
+ setLoading(true);
91
+ setError(null);
92
+ try {
93
+ await deleteService.handle(id);
94
+ setItems((prev) => prev.filter((item) => item.id !== id));
95
+ } catch (err: unknown) {
96
+ setError(err instanceof Error ? err.message : 'Failed to delete');
97
+ } finally {
98
+ setLoading(false);
99
+ }
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ fetchAll();
104
+ }, [fetchAll]);
105
+
106
+ return { items, selected, loading, error, fetchAll, fetchOne, create, update, remove };
107
+ }
108
+ `;
109
+ }
110
+
111
+ export function makeHook(feature: string, name: string, pascalName?: string): void {
112
+ const base = ensureFeatureExists(feature, 'hooks');
113
+ const filePath = path.join(base, `${name}.ts`);
114
+
115
+ if (fileExists(filePath)) {
116
+ throw new Error(`Hook "${name}" already exists at ${filePath}`);
117
+ }
118
+
119
+ const entityName = pascalName ?? name.replace(/^use/, '');
120
+ writeFileSafe(filePath, renderHook(name, entityName, feature));
121
+ console.log(`✅ Hook "${name}" created at ${filePath}`);
122
+ }
@@ -0,0 +1,95 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists, resolveImport } from '../utils';
3
+
4
+ type RepositoryAction = 'List' | 'Create' | 'Update' | 'Delete' | 'Show';
5
+
6
+ const REPOSITORY_ACTIONS: RepositoryAction[] = ['List', 'Create', 'Update', 'Delete', 'Show'];
7
+
8
+ function renderRepository(action: RepositoryAction, name: string, feature: string): string {
9
+ const typePath = resolveImport(feature, `types/${name}.types`);
10
+ const schemaPath = (op: string) => resolveImport(feature, `schemas/${op}${name}.schema`);
11
+
12
+ switch (action) {
13
+ case 'List':
14
+ return `import { ${name} } from '${typePath}';
15
+
16
+ export class List${name}Repository {
17
+ async handle(): Promise<${name}[]> {
18
+ const response = await fetch('/api/${feature}');
19
+ if (!response.ok) throw new Error('Failed to fetch ${name} list');
20
+ return response.json();
21
+ }
22
+ }
23
+ `;
24
+ case 'Show':
25
+ return `import { ${name} } from '${typePath}';
26
+
27
+ export class Show${name}Repository {
28
+ async handle(id: string): Promise<${name}> {
29
+ const response = await fetch(\`/api/${feature}/\${id}\`);
30
+ if (!response.ok) throw new Error('Failed to fetch ${name}');
31
+ return response.json();
32
+ }
33
+ }
34
+ `;
35
+ case 'Create':
36
+ return `import { ${name} } from '${typePath}';
37
+ import { Create${name} } from '${schemaPath('Create')}';
38
+
39
+ export class Create${name}Repository {
40
+ async handle(data: Create${name}): Promise<${name}> {
41
+ const response = await fetch('/api/${feature}', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify(data),
45
+ });
46
+ if (!response.ok) throw new Error('Failed to create ${name}');
47
+ return response.json();
48
+ }
49
+ }
50
+ `;
51
+ case 'Update':
52
+ return `import { ${name} } from '${typePath}';
53
+ import { Update${name} } from '${schemaPath('Update')}';
54
+
55
+ export class Update${name}Repository {
56
+ async handle(id: string, data: Update${name}): Promise<${name}> {
57
+ const response = await fetch(\`/api/${feature}/\${id}\`, {
58
+ method: 'PUT',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify(data),
61
+ });
62
+ if (!response.ok) throw new Error('Failed to update ${name}');
63
+ return response.json();
64
+ }
65
+ }
66
+ `;
67
+ case 'Delete':
68
+ return `export class Delete${name}Repository {
69
+ async handle(id: string): Promise<void> {
70
+ const response = await fetch(\`/api/${feature}/\${id}\`, {
71
+ method: 'DELETE',
72
+ });
73
+ if (!response.ok) throw new Error('Failed to delete ${name}');
74
+ }
75
+ }
76
+ `;
77
+ }
78
+ }
79
+
80
+ export function makeRepository(feature: string, name: string): void {
81
+ const base = ensureFeatureExists(feature, 'repositories');
82
+
83
+ for (const action of REPOSITORY_ACTIONS) {
84
+ const filePath = path.join(base, `${action}${name}.repository.ts`);
85
+
86
+ if (fileExists(filePath)) {
87
+ console.warn(`⚠️ Skipping "${action}${name}.repository.ts" — already exists`);
88
+ continue;
89
+ }
90
+
91
+ writeFileSafe(filePath, renderRepository(action, name, feature));
92
+ }
93
+
94
+ console.log(`✅ Repositories for "${name}" created at ${base}`);
95
+ }
@@ -0,0 +1,47 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists } from '../utils';
3
+
4
+ type SchemaAction = 'Create' | 'Update';
5
+
6
+ const SCHEMA_ACTIONS: SchemaAction[] = ['Create', 'Update'];
7
+
8
+ function renderSchema(action: SchemaAction, name: string): string {
9
+ switch (action) {
10
+ case 'Create':
11
+ return `import { z } from 'zod';
12
+
13
+ export const Create${name}Schema = z.object({
14
+ // add create fields here
15
+ });
16
+
17
+ export type Create${name} = z.infer<typeof Create${name}Schema>;
18
+ `;
19
+ case 'Update':
20
+ return `import { z } from 'zod';
21
+
22
+ export const Update${name}Schema = z.object({
23
+ id: z.string(),
24
+ // add update fields here
25
+ });
26
+
27
+ export type Update${name} = z.infer<typeof Update${name}Schema>;
28
+ `;
29
+ }
30
+ }
31
+
32
+ export function makeSchema(feature: string, name: string): void {
33
+ const base = ensureFeatureExists(feature, 'schemas');
34
+
35
+ for (const action of SCHEMA_ACTIONS) {
36
+ const filePath = path.join(base, `${action}${name}.schema.ts`);
37
+
38
+ if (fileExists(filePath)) {
39
+ console.warn(`⚠️ Skipping "${action}${name}.schema.ts" — already exists`);
40
+ continue;
41
+ }
42
+
43
+ writeFileSafe(filePath, renderSchema(action, name));
44
+ }
45
+
46
+ console.log(`✅ Schemas for "${name}" created at ${base}`);
47
+ }
@@ -0,0 +1,93 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists, resolveImport } from '../utils';
3
+
4
+ type ServiceAction = 'List' | 'Create' | 'Update' | 'Delete' | 'Show';
5
+
6
+ const SERVICE_ACTIONS: ServiceAction[] = ['List', 'Create', 'Update', 'Delete', 'Show'];
7
+
8
+ function renderService(action: ServiceAction, name: string, feature: string): string {
9
+ const typePath = resolveImport(feature, `types/${name}.types`);
10
+ const repoPath = (op: string) => resolveImport(feature, `repositories/${op}${name}.repository`);
11
+ const schemaPath = (op: string) => resolveImport(feature, `schemas/${op}${name}.schema`);
12
+
13
+ switch (action) {
14
+ case 'List':
15
+ return `import { ${name} } from '${typePath}';
16
+ import { List${name}Repository } from '${repoPath('List')}';
17
+
18
+ const repository = new List${name}Repository();
19
+
20
+ export class List${name}Service {
21
+ async handle(): Promise<${name}[]> {
22
+ return repository.handle();
23
+ }
24
+ }
25
+ `;
26
+ case 'Show':
27
+ return `import { ${name} } from '${typePath}';
28
+ import { Show${name}Repository } from '${repoPath('Show')}';
29
+
30
+ const repository = new Show${name}Repository();
31
+
32
+ export class Show${name}Service {
33
+ async handle(id: string): Promise<${name}> {
34
+ return repository.handle(id);
35
+ }
36
+ }
37
+ `;
38
+ case 'Create':
39
+ return `import { ${name} } from '${typePath}';
40
+ import { Create${name} } from '${schemaPath('Create')}';
41
+ import { Create${name}Repository } from '${repoPath('Create')}';
42
+
43
+ const repository = new Create${name}Repository();
44
+
45
+ export class Create${name}Service {
46
+ async handle(data: Create${name}): Promise<${name}> {
47
+ return repository.handle(data);
48
+ }
49
+ }
50
+ `;
51
+ case 'Update':
52
+ return `import { ${name} } from '${typePath}';
53
+ import { Update${name} } from '${schemaPath('Update')}';
54
+ import { Update${name}Repository } from '${repoPath('Update')}';
55
+
56
+ const repository = new Update${name}Repository();
57
+
58
+ export class Update${name}Service {
59
+ async handle(id: string, data: Update${name}): Promise<${name}> {
60
+ return repository.handle(id, data);
61
+ }
62
+ }
63
+ `;
64
+ case 'Delete':
65
+ return `import { Delete${name}Repository } from '${repoPath('Delete')}';
66
+
67
+ const repository = new Delete${name}Repository();
68
+
69
+ export class Delete${name}Service {
70
+ async handle(id: string): Promise<void> {
71
+ return repository.handle(id);
72
+ }
73
+ }
74
+ `;
75
+ }
76
+ }
77
+
78
+ export function makeService(feature: string, name: string): void {
79
+ const base = ensureFeatureExists(feature, 'services');
80
+
81
+ for (const action of SERVICE_ACTIONS) {
82
+ const filePath = path.join(base, `${action}${name}.service.ts`);
83
+
84
+ if (fileExists(filePath)) {
85
+ console.warn(`⚠️ Skipping "${action}${name}.service.ts" — already exists`);
86
+ continue;
87
+ }
88
+
89
+ writeFileSafe(filePath, renderService(action, name, feature));
90
+ }
91
+
92
+ console.log(`✅ Services for "${name}" created at ${base}`);
93
+ }
@@ -0,0 +1,25 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists } from '../utils';
3
+
4
+ function renderTypes(name: string): string {
5
+ return `export interface ${name} {
6
+ id: string;
7
+ // add ${name} fields here
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+ `;
12
+ }
13
+
14
+ export function makeTypes(feature: string, name: string): void {
15
+ const base = ensureFeatureExists(feature, 'types');
16
+ const filePath = path.join(base, `${name}.types.ts`);
17
+
18
+ if (fileExists(filePath)) {
19
+ console.warn(`⚠️ Skipping "${name}.types.ts" — already exists`);
20
+ return;
21
+ }
22
+
23
+ writeFileSafe(filePath, renderTypes(name));
24
+ console.log(`✅ Types for "${name}" created at ${filePath}`);
25
+ }
package/src/index.ts CHANGED
@@ -1,16 +1,126 @@
1
1
  #!/usr/bin/env node
2
+ import { Command } from 'commander';
2
3
  import { makeFeature } from './commands/feature';
4
+ import { makeComponent } from './commands/component';
5
+ import { makeHook } from './commands/hook';
6
+ import { makeService } from './commands/service';
7
+ import { makeSchema } from './commands/schema';
8
+ import { makeRepository } from './commands/repository';
9
+ import { makeContainer } from './commands/container';
10
+ import { makeTypes } from './commands/types';
3
11
 
4
- const [,, command, name] = process.argv;
12
+ const program = new Command();
5
13
 
6
- if (!command || !name) {
7
- console.error('Usage: domain-driver make:feature <name>');
8
- process.exit(1);
9
- }
14
+ program
15
+ .name('domain-driver')
16
+ .description('CLI scaffolding tool for domain-driven development in Next.js')
17
+ .version('0.1.0');
10
18
 
11
- if (command === 'make:feature') {
12
- makeFeature(name);
13
- } else {
14
- console.error(`Unknown command: ${command}`);
15
- process.exit(1);
16
- }
19
+ program
20
+ .command('make:feature <name>')
21
+ .description('Scaffold a full feature folder structure')
22
+ .option('-a, --all', 'Scaffold all files inside each folder')
23
+ .action(async (name: string, options: { all?: boolean }) => {
24
+ try {
25
+ await makeFeature(name, options.all ?? false);
26
+ } catch (error: unknown) {
27
+ const message = error instanceof Error ? error.message : 'Unknown error';
28
+ console.error(`❌ ${message}`);
29
+ process.exit(1);
30
+ }
31
+ });
32
+
33
+ program
34
+ .command('make:component <feature> <name>')
35
+ .description('Scaffold a component inside an existing feature')
36
+ .argument('[type]', 'Component type: client or server', 'client')
37
+ .action((feature: string, name: string, type: string) => {
38
+ try {
39
+ const componentType = type === 'server' ? 'server' : 'client';
40
+ makeComponent(feature, name, componentType);
41
+ } catch (error: unknown) {
42
+ const message = error instanceof Error ? error.message : 'Unknown error';
43
+ console.error(`❌ ${message}`);
44
+ process.exit(1);
45
+ }
46
+ });
47
+
48
+ program
49
+ .command('make:container <feature> <name>')
50
+ .description('Scaffold a smart container component inside an existing feature')
51
+ .action((feature: string, name: string) => {
52
+ try {
53
+ makeContainer(feature, name);
54
+ } catch (error: unknown) {
55
+ const message = error instanceof Error ? error.message : 'Unknown error';
56
+ console.error(`❌ ${message}`);
57
+ process.exit(1);
58
+ }
59
+ });
60
+
61
+ program
62
+ .command('make:hook <feature> <name>')
63
+ .description('Scaffold a custom hook inside an existing feature')
64
+ .action((feature: string, name: string) => {
65
+ try {
66
+ makeHook(feature, name);
67
+ } catch (error: unknown) {
68
+ const message = error instanceof Error ? error.message : 'Unknown error';
69
+ console.error(`❌ ${message}`);
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ program
75
+ .command('make:service <feature> <name>')
76
+ .description('Scaffold single-responsibility service files inside an existing feature')
77
+ .action((feature: string, name: string) => {
78
+ try {
79
+ makeService(feature, name);
80
+ } catch (error: unknown) {
81
+ const message = error instanceof Error ? error.message : 'Unknown error';
82
+ console.error(`❌ ${message}`);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ program
88
+ .command('make:repository <feature> <name>')
89
+ .description('Scaffold single-responsibility repository files inside an existing feature')
90
+ .action((feature: string, name: string) => {
91
+ try {
92
+ makeRepository(feature, name);
93
+ } catch (error: unknown) {
94
+ const message = error instanceof Error ? error.message : 'Unknown error';
95
+ console.error(`❌ ${message}`);
96
+ process.exit(1);
97
+ }
98
+ });
99
+
100
+ program
101
+ .command('make:schema <feature> <name>')
102
+ .description('Scaffold Zod schemas for create and update operations')
103
+ .action((feature: string, name: string) => {
104
+ try {
105
+ makeSchema(feature, name);
106
+ } catch (error: unknown) {
107
+ const message = error instanceof Error ? error.message : 'Unknown error';
108
+ console.error(`❌ ${message}`);
109
+ process.exit(1);
110
+ }
111
+ });
112
+
113
+ program
114
+ .command('make:types <feature> <name>')
115
+ .description('Scaffold a types file inside an existing feature')
116
+ .action((feature: string, name: string) => {
117
+ try {
118
+ makeTypes(feature, name);
119
+ } catch (error: unknown) {
120
+ const message = error instanceof Error ? error.message : 'Unknown error';
121
+ console.error(`❌ ${message}`);
122
+ process.exit(1);
123
+ }
124
+ });
125
+
126
+ program.parse();
package/src/utils.ts ADDED
@@ -0,0 +1,115 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ export function toPascalCase(name: string): string {
5
+ return name
6
+ .split('-')
7
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
8
+ .join('');
9
+ }
10
+
11
+ export function featureBasePath(feature: string): string {
12
+ return path.join(process.cwd(), 'app', feature);
13
+ }
14
+
15
+ export function ensureFeatureExists(feature: string, subdir: string): string {
16
+ const base = path.join(featureBasePath(feature), subdir);
17
+
18
+ if (!fs.existsSync(base)) {
19
+ throw new Error(
20
+ `Feature "${feature}" does not exist. Run: domain-driver make:feature ${feature}`
21
+ );
22
+ }
23
+
24
+ return base;
25
+ }
26
+
27
+ export function writeFileSafe(filePath: string, content: string): void {
28
+ try {
29
+ fs.writeFileSync(filePath, content);
30
+ } catch (error: unknown) {
31
+ const message = error instanceof Error ? error.message : 'Unknown error';
32
+ throw new Error(`Failed to write ${filePath}: ${message}`);
33
+ }
34
+ }
35
+
36
+ export function mkdirSafe(dirPath: string): void {
37
+ try {
38
+ fs.mkdirSync(dirPath, { recursive: true });
39
+ } catch (error: unknown) {
40
+ const message = error instanceof Error ? error.message : 'Unknown error';
41
+ throw new Error(`Failed to create directory ${dirPath}: ${message}`);
42
+ }
43
+ }
44
+
45
+ export function fileExists(filePath: string): boolean {
46
+ return fs.existsSync(filePath);
47
+ }
48
+
49
+ interface AliasConfig {
50
+ alias: string | null;
51
+ appDir: string;
52
+ }
53
+
54
+ let cachedAlias: AliasConfig | undefined;
55
+
56
+ export function detectAlias(): AliasConfig {
57
+ if (cachedAlias) return cachedAlias;
58
+
59
+ const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
60
+
61
+ if (!fs.existsSync(tsconfigPath)) {
62
+ cachedAlias = { alias: null, appDir: 'app' };
63
+ return cachedAlias;
64
+ }
65
+
66
+ try {
67
+ const raw = fs.readFileSync(tsconfigPath, 'utf-8');
68
+ const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
69
+ const tsconfig = JSON.parse(stripped);
70
+ const paths = tsconfig?.compilerOptions?.paths;
71
+
72
+ if (paths) {
73
+ for (const [key, values] of Object.entries(paths)) {
74
+ const targets = values as string[];
75
+ const hasAppMapping = targets.some(
76
+ (v) => v === './app/*' || v === 'app/*' || v === './src/app/*' || v === 'src/app/*'
77
+ );
78
+ if (hasAppMapping && key.endsWith('/*')) {
79
+ const prefix = key.slice(0, -1);
80
+ const appDir = targets[0].replace('/*', '').replace('./', '');
81
+ cachedAlias = { alias: prefix, appDir };
82
+ return cachedAlias;
83
+ }
84
+
85
+ const hasSrcMapping = targets.some(
86
+ (v) => v === './src/*' || v === 'src/*' || v === './*'
87
+ );
88
+ if (hasSrcMapping && key.endsWith('/*')) {
89
+ const prefix = key.slice(0, -1);
90
+ cachedAlias = { alias: prefix, appDir: 'app' };
91
+ return cachedAlias;
92
+ }
93
+ }
94
+ }
95
+ } catch {
96
+ // tsconfig parse failed, fall back to relative imports
97
+ }
98
+
99
+ cachedAlias = { alias: null, appDir: 'app' };
100
+ return cachedAlias;
101
+ }
102
+
103
+ export function resetAliasCache(): void {
104
+ cachedAlias = undefined;
105
+ }
106
+
107
+ export function resolveImport(fromFeature: string, relativePath: string, fromRoot: boolean = false): string {
108
+ const { alias } = detectAlias();
109
+
110
+ if (alias) {
111
+ return `${alias}app/${fromFeature}/${relativePath}`;
112
+ }
113
+
114
+ return fromRoot ? `./${relativePath}` : `../${relativePath}`;
115
+ }
package/tsconfig.json CHANGED
@@ -9,5 +9,5 @@
9
9
  "types": ["node"]
10
10
  },
11
11
  "include": ["src/**/*"],
12
- "exclude": ["node_modules", "dist"]
12
+ "exclude": ["node_modules", "dist", "src/**/__tests__/**"]
13
13
  }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/__tests__/**/*.test.ts'],
6
+ },
7
+ });