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,128 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { toPascalCase, detectAlias, resolveImport, resetAliasCache } from '../utils';
6
+
7
+ describe('toPascalCase', () => {
8
+ it('converts kebab-case to PascalCase', () => {
9
+ expect(toPascalCase('coffee-type')).toBe('CoffeeType');
10
+ });
11
+
12
+ it('handles single word', () => {
13
+ expect(toPascalCase('user')).toBe('User');
14
+ });
15
+
16
+ it('handles multiple hyphens', () => {
17
+ expect(toPascalCase('my-long-feature-name')).toBe('MyLongFeatureName');
18
+ });
19
+ });
20
+
21
+ describe('detectAlias', () => {
22
+ let testDir: string;
23
+ let originalCwd: string;
24
+
25
+ beforeEach(() => {
26
+ originalCwd = process.cwd();
27
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-alias-'));
28
+ process.chdir(testDir);
29
+ resetAliasCache();
30
+ });
31
+
32
+ afterEach(() => {
33
+ process.chdir(originalCwd);
34
+ fs.rmSync(testDir, { recursive: true, force: true });
35
+ });
36
+
37
+ it('returns null alias when no tsconfig exists', () => {
38
+ const result = detectAlias();
39
+ expect(result.alias).toBeNull();
40
+ });
41
+
42
+ it('detects @/ alias mapping to ./app/*', () => {
43
+ fs.writeFileSync(
44
+ path.join(testDir, 'tsconfig.json'),
45
+ JSON.stringify({
46
+ compilerOptions: {
47
+ paths: { '@/*': ['./app/*'] },
48
+ },
49
+ })
50
+ );
51
+ const result = detectAlias();
52
+ expect(result.alias).toBe('@/');
53
+ });
54
+
55
+ it('detects @/ alias mapping to ./src/*', () => {
56
+ fs.writeFileSync(
57
+ path.join(testDir, 'tsconfig.json'),
58
+ JSON.stringify({
59
+ compilerOptions: {
60
+ paths: { '@/*': ['./src/*'] },
61
+ },
62
+ })
63
+ );
64
+ const result = detectAlias();
65
+ expect(result.alias).toBe('@/');
66
+ });
67
+
68
+ it('detects custom alias like ~/*', () => {
69
+ fs.writeFileSync(
70
+ path.join(testDir, 'tsconfig.json'),
71
+ JSON.stringify({
72
+ compilerOptions: {
73
+ paths: { '~/*': ['./app/*'] },
74
+ },
75
+ })
76
+ );
77
+ const result = detectAlias();
78
+ expect(result.alias).toBe('~/');
79
+ });
80
+
81
+ it('returns null when paths has no matching pattern', () => {
82
+ fs.writeFileSync(
83
+ path.join(testDir, 'tsconfig.json'),
84
+ JSON.stringify({
85
+ compilerOptions: {
86
+ paths: { '@components/*': ['./components/*'] },
87
+ },
88
+ })
89
+ );
90
+ const result = detectAlias();
91
+ expect(result.alias).toBeNull();
92
+ });
93
+ });
94
+
95
+ describe('resolveImport', () => {
96
+ let testDir: string;
97
+ let originalCwd: string;
98
+
99
+ beforeEach(() => {
100
+ originalCwd = process.cwd();
101
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-resolve-'));
102
+ process.chdir(testDir);
103
+ resetAliasCache();
104
+ });
105
+
106
+ afterEach(() => {
107
+ process.chdir(originalCwd);
108
+ fs.rmSync(testDir, { recursive: true, force: true });
109
+ });
110
+
111
+ it('uses relative path when no alias', () => {
112
+ const result = resolveImport('coffee-type', 'hooks/useCoffeeType');
113
+ expect(result).toBe('../hooks/useCoffeeType');
114
+ });
115
+
116
+ it('uses alias path when @/ alias exists', () => {
117
+ fs.writeFileSync(
118
+ path.join(testDir, 'tsconfig.json'),
119
+ JSON.stringify({
120
+ compilerOptions: {
121
+ paths: { '@/*': ['./app/*'] },
122
+ },
123
+ })
124
+ );
125
+ const result = resolveImport('coffee-type', 'hooks/useCoffeeType');
126
+ expect(result).toBe('@/app/coffee-type/hooks/useCoffeeType');
127
+ });
128
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { makeFeature } from '../feature';
6
+ import { resetAliasCache } from '../../utils';
7
+
8
+ let testDir: string;
9
+ let originalCwd: string;
10
+
11
+ function cleanup(): void {
12
+ process.chdir(originalCwd);
13
+ resetAliasCache();
14
+ if (fs.existsSync(testDir)) {
15
+ fs.rmSync(testDir, { recursive: true, force: true });
16
+ }
17
+ }
18
+
19
+ describe('make:feature', () => {
20
+ beforeEach(() => {
21
+ originalCwd = process.cwd();
22
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-feature-'));
23
+ process.chdir(testDir);
24
+ resetAliasCache();
25
+ });
26
+ afterEach(cleanup);
27
+
28
+ it('creates feature directories with .gitkeep files', async () => {
29
+ await makeFeature('test-feature', false);
30
+
31
+ const base = path.join(testDir, 'app', 'test-feature');
32
+ expect(fs.existsSync(base)).toBe(true);
33
+ expect(fs.existsSync(path.join(base, 'components/client/.gitkeep'))).toBe(true);
34
+ expect(fs.existsSync(path.join(base, 'components/server/.gitkeep'))).toBe(true);
35
+ expect(fs.existsSync(path.join(base, 'containers/.gitkeep'))).toBe(true);
36
+ expect(fs.existsSync(path.join(base, 'hooks/.gitkeep'))).toBe(true);
37
+ expect(fs.existsSync(path.join(base, 'services/.gitkeep'))).toBe(true);
38
+ expect(fs.existsSync(path.join(base, 'repositories/.gitkeep'))).toBe(true);
39
+ expect(fs.existsSync(path.join(base, 'schemas/.gitkeep'))).toBe(true);
40
+ expect(fs.existsSync(path.join(base, 'types/.gitkeep'))).toBe(true);
41
+ });
42
+
43
+ it('creates page.tsx with PascalCase component name', async () => {
44
+ await makeFeature('test-feature', false);
45
+
46
+ const page = fs.readFileSync(path.join(testDir, 'app/test-feature/page.tsx'), 'utf-8');
47
+ expect(page).toContain('TestFeaturePage');
48
+ expect(page).toContain('export default function');
49
+ });
50
+
51
+ it('throws if feature already exists', async () => {
52
+ await makeFeature('test-feature', false);
53
+ await expect(makeFeature('test-feature', false)).rejects.toThrow('already exists');
54
+ });
55
+
56
+ it('scaffolds all files with -a flag', async () => {
57
+ await makeFeature('coffee-type', true);
58
+
59
+ const base = path.join(testDir, 'app', 'coffee-type');
60
+
61
+ // No .gitkeep files when -a is used
62
+ expect(fs.existsSync(path.join(base, 'components/client/.gitkeep'))).toBe(false);
63
+
64
+ // All files created with correct names
65
+ expect(fs.existsSync(path.join(base, 'page.tsx'))).toBe(true);
66
+ expect(fs.existsSync(path.join(base, 'components/client/CoffeeType.tsx'))).toBe(true);
67
+ expect(fs.existsSync(path.join(base, 'containers/CoffeeTypeContainer.tsx'))).toBe(true);
68
+ expect(fs.existsSync(path.join(base, 'hooks/useCoffeeType.ts'))).toBe(true);
69
+ expect(fs.existsSync(path.join(base, 'schemas/CreateCoffeeType.schema.ts'))).toBe(true);
70
+ expect(fs.existsSync(path.join(base, 'schemas/UpdateCoffeeType.schema.ts'))).toBe(true);
71
+ expect(fs.existsSync(path.join(base, 'services/ListCoffeeType.service.ts'))).toBe(true);
72
+ expect(fs.existsSync(path.join(base, 'services/CreateCoffeeType.service.ts'))).toBe(true);
73
+ expect(fs.existsSync(path.join(base, 'repositories/ListCoffeeType.repository.ts'))).toBe(true);
74
+ expect(fs.existsSync(path.join(base, 'repositories/CreateCoffeeType.repository.ts'))).toBe(true);
75
+ expect(fs.existsSync(path.join(base, 'types/CoffeeType.types.ts'))).toBe(true);
76
+ });
77
+
78
+ it('page imports container when -a flag is used', async () => {
79
+ await makeFeature('coffee-type', true);
80
+
81
+ const page = fs.readFileSync(path.join(testDir, 'app/coffee-type/page.tsx'), 'utf-8');
82
+ expect(page).toContain("import CoffeeTypeContainer from './containers/CoffeeTypeContainer'");
83
+ expect(page).toContain('<CoffeeTypeContainer />');
84
+ });
85
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { makeFeature } from '../feature';
6
+ import { resetAliasCache } from '../../utils';
7
+
8
+ let testDir: string;
9
+ let originalCwd: string;
10
+
11
+ function readFile(relativePath: string): string {
12
+ return fs.readFileSync(path.join(testDir, 'app', relativePath), 'utf-8');
13
+ }
14
+
15
+ function cleanup(): void {
16
+ process.chdir(originalCwd);
17
+ resetAliasCache();
18
+ if (fs.existsSync(testDir)) {
19
+ fs.rmSync(testDir, { recursive: true, force: true });
20
+ }
21
+ }
22
+
23
+ describe('inter-layer imports (no alias)', () => {
24
+ beforeEach(async () => {
25
+ originalCwd = process.cwd();
26
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-imports-'));
27
+ process.chdir(testDir);
28
+ resetAliasCache();
29
+ await makeFeature('coffee-type', true);
30
+ });
31
+ afterEach(cleanup);
32
+
33
+ it('types file is created with interface', () => {
34
+ const content = readFile('coffee-type/types/CoffeeType.types.ts');
35
+ expect(content).toContain('export interface CoffeeType');
36
+ expect(content).toContain('id: string');
37
+ });
38
+
39
+ it('container imports hook and component with relative paths', () => {
40
+ const content = readFile('coffee-type/containers/CoffeeTypeContainer.tsx');
41
+ expect(content).toContain("from '../hooks/useCoffeeType'");
42
+ expect(content).toContain("from '../components/client/CoffeeType'");
43
+ });
44
+
45
+ it('hook imports type, services, and schemas with relative paths', () => {
46
+ const content = readFile('coffee-type/hooks/useCoffeeType.ts');
47
+ expect(content).toContain("import { CoffeeType } from '../types/CoffeeType.types'");
48
+ expect(content).toContain("from '../services/ListCoffeeType.service'");
49
+ expect(content).toContain("from '../schemas/CreateCoffeeType.schema'");
50
+ });
51
+
52
+ it('services import type and repositories with relative paths', () => {
53
+ const content = readFile('coffee-type/services/CreateCoffeeType.service.ts');
54
+ expect(content).toContain("import { CoffeeType } from '../types/CoffeeType.types'");
55
+ expect(content).toContain("from '../repositories/CreateCoffeeType.repository'");
56
+ });
57
+
58
+ it('repositories import type and schema types', () => {
59
+ const create = readFile('coffee-type/repositories/CreateCoffeeType.repository.ts');
60
+ expect(create).toContain("import { CoffeeType } from '../types/CoffeeType.types'");
61
+ expect(create).toContain("from '../schemas/CreateCoffeeType.schema'");
62
+
63
+ const list = readFile('coffee-type/repositories/ListCoffeeType.repository.ts');
64
+ expect(list).toContain("import { CoffeeType } from '../types/CoffeeType.types'");
65
+ });
66
+
67
+ it('delete repository has no schema import but has type import', () => {
68
+ const del = readFile('coffee-type/repositories/DeleteCoffeeType.repository.ts');
69
+ expect(del).not.toContain('import');
70
+ });
71
+ });
72
+
73
+ describe('inter-layer imports (with @/ alias)', () => {
74
+ beforeEach(async () => {
75
+ originalCwd = process.cwd();
76
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-alias-imports-'));
77
+ process.chdir(testDir);
78
+ resetAliasCache();
79
+
80
+ fs.writeFileSync(
81
+ path.join(testDir, 'tsconfig.json'),
82
+ JSON.stringify({
83
+ compilerOptions: {
84
+ paths: { '@/*': ['./app/*'] },
85
+ },
86
+ })
87
+ );
88
+
89
+ await makeFeature('coffee-type', true);
90
+ });
91
+ afterEach(cleanup);
92
+
93
+ it('container imports use @/ alias', () => {
94
+ const content = readFile('coffee-type/containers/CoffeeTypeContainer.tsx');
95
+ expect(content).toContain("from '@/app/coffee-type/hooks/useCoffeeType'");
96
+ expect(content).toContain("from '@/app/coffee-type/components/client/CoffeeType'");
97
+ });
98
+
99
+ it('hook imports use @/ alias', () => {
100
+ const content = readFile('coffee-type/hooks/useCoffeeType.ts');
101
+ expect(content).toContain("from '@/app/coffee-type/types/CoffeeType.types'");
102
+ expect(content).toContain("from '@/app/coffee-type/services/ListCoffeeType.service'");
103
+ });
104
+
105
+ it('services import repositories with @/ alias', () => {
106
+ const content = readFile('coffee-type/services/CreateCoffeeType.service.ts');
107
+ expect(content).toContain("from '@/app/coffee-type/types/CoffeeType.types'");
108
+ expect(content).toContain("from '@/app/coffee-type/repositories/CreateCoffeeType.repository'");
109
+ });
110
+
111
+ it('page imports container with @/ alias', () => {
112
+ const content = readFile('coffee-type/page.tsx');
113
+ expect(content).toContain("from '@/app/coffee-type/containers/CoffeeTypeContainer'");
114
+ });
115
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { makeFeature } from '../feature';
6
+ import { makeComponent } from '../component';
7
+ import { makeContainer } from '../container';
8
+ import { makeHook } from '../hook';
9
+ import { makeService } from '../service';
10
+ import { makeRepository } from '../repository';
11
+ import { makeSchema } from '../schema';
12
+ import { resetAliasCache } from '../../utils';
13
+
14
+ let testDir: string;
15
+ let originalCwd: string;
16
+
17
+ function cleanup(): void {
18
+ process.chdir(originalCwd);
19
+ resetAliasCache();
20
+ if (fs.existsSync(testDir)) {
21
+ fs.rmSync(testDir, { recursive: true, force: true });
22
+ }
23
+ }
24
+
25
+ describe('individual commands on existing feature', () => {
26
+ beforeEach(async () => {
27
+ originalCwd = process.cwd();
28
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dd-individual-'));
29
+ process.chdir(testDir);
30
+ resetAliasCache();
31
+ await makeFeature('test-feature', false);
32
+ });
33
+ afterEach(cleanup);
34
+
35
+ it('make:component creates client component by default', () => {
36
+ makeComponent('test-feature', 'MyButton', 'client');
37
+ const content = fs.readFileSync(
38
+ path.join(testDir, 'app/test-feature/components/client/MyButton.tsx'),
39
+ 'utf-8'
40
+ );
41
+ expect(content).toContain("'use client'");
42
+ expect(content).toContain('export default function MyButton');
43
+ expect(content).toContain('interface MyButtonProps');
44
+ });
45
+
46
+ it('make:component creates server component', () => {
47
+ makeComponent('test-feature', 'DataTable', 'server');
48
+ const content = fs.readFileSync(
49
+ path.join(testDir, 'app/test-feature/components/server/DataTable.tsx'),
50
+ 'utf-8'
51
+ );
52
+ expect(content).not.toContain("'use client'");
53
+ expect(content).toContain('export default function DataTable');
54
+ });
55
+
56
+ it('make:component throws if component already exists', () => {
57
+ makeComponent('test-feature', 'MyButton', 'client');
58
+ expect(() => makeComponent('test-feature', 'MyButton', 'client')).toThrow('already exists');
59
+ });
60
+
61
+ it('make:container creates container', () => {
62
+ makeContainer('test-feature', 'TestContainer');
63
+ const content = fs.readFileSync(
64
+ path.join(testDir, 'app/test-feature/containers/TestContainer.tsx'),
65
+ 'utf-8'
66
+ );
67
+ expect(content).toContain("'use client'");
68
+ expect(content).toContain('export default function TestContainer');
69
+ });
70
+
71
+ it('make:hook creates hook file', () => {
72
+ makeHook('test-feature', 'useTest');
73
+ const content = fs.readFileSync(
74
+ path.join(testDir, 'app/test-feature/hooks/useTest.ts'),
75
+ 'utf-8'
76
+ );
77
+ expect(content).toContain('export function useTest');
78
+ expect(content).toContain('useState');
79
+ });
80
+
81
+ it('make:service creates 5 service files', () => {
82
+ makeService('test-feature', 'TestEntity');
83
+ const base = path.join(testDir, 'app/test-feature/services');
84
+ expect(fs.existsSync(path.join(base, 'ListTestEntity.service.ts'))).toBe(true);
85
+ expect(fs.existsSync(path.join(base, 'ShowTestEntity.service.ts'))).toBe(true);
86
+ expect(fs.existsSync(path.join(base, 'CreateTestEntity.service.ts'))).toBe(true);
87
+ expect(fs.existsSync(path.join(base, 'UpdateTestEntity.service.ts'))).toBe(true);
88
+ expect(fs.existsSync(path.join(base, 'DeleteTestEntity.service.ts'))).toBe(true);
89
+ });
90
+
91
+ it('make:service skips existing files', () => {
92
+ makeService('test-feature', 'TestEntity');
93
+ expect(() => makeService('test-feature', 'TestEntity')).not.toThrow();
94
+ });
95
+
96
+ it('make:repository creates 5 repository files', () => {
97
+ makeRepository('test-feature', 'TestEntity');
98
+ const base = path.join(testDir, 'app/test-feature/repositories');
99
+ expect(fs.existsSync(path.join(base, 'ListTestEntity.repository.ts'))).toBe(true);
100
+ expect(fs.existsSync(path.join(base, 'ShowTestEntity.repository.ts'))).toBe(true);
101
+ expect(fs.existsSync(path.join(base, 'CreateTestEntity.repository.ts'))).toBe(true);
102
+ expect(fs.existsSync(path.join(base, 'UpdateTestEntity.repository.ts'))).toBe(true);
103
+ expect(fs.existsSync(path.join(base, 'DeleteTestEntity.repository.ts'))).toBe(true);
104
+ });
105
+
106
+ it('make:repository generates proper fetch calls', () => {
107
+ makeRepository('test-feature', 'TestEntity');
108
+ const content = fs.readFileSync(
109
+ path.join(testDir, 'app/test-feature/repositories/CreateTestEntity.repository.ts'),
110
+ 'utf-8'
111
+ );
112
+ expect(content).toContain("fetch('/api/test-feature'");
113
+ expect(content).toContain("method: 'POST'");
114
+ expect(content).toContain("'Content-Type': 'application/json'");
115
+ });
116
+
117
+ it('make:schema creates Create and Update schemas', () => {
118
+ makeSchema('test-feature', 'TestEntity');
119
+ const base = path.join(testDir, 'app/test-feature/schemas');
120
+
121
+ const create = fs.readFileSync(path.join(base, 'CreateTestEntity.schema.ts'), 'utf-8');
122
+ expect(create).toContain("import { z } from 'zod'");
123
+ expect(create).toContain('CreateTestEntity');
124
+
125
+ const update = fs.readFileSync(path.join(base, 'UpdateTestEntity.schema.ts'), 'utf-8');
126
+ expect(update).toContain('id: z.string()');
127
+ });
128
+
129
+ it('throws when feature does not exist', () => {
130
+ expect(() => makeComponent('nonexistent', 'Foo', 'client')).toThrow('does not exist');
131
+ expect(() => makeHook('nonexistent', 'useFoo')).toThrow('does not exist');
132
+ expect(() => makeService('nonexistent', 'Foo')).toThrow('does not exist');
133
+ expect(() => makeRepository('nonexistent', 'Foo')).toThrow('does not exist');
134
+ expect(() => makeSchema('nonexistent', 'Foo')).toThrow('does not exist');
135
+ expect(() => makeContainer('nonexistent', 'FooContainer')).toThrow('does not exist');
136
+ });
137
+ });
@@ -0,0 +1,33 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists } from '../utils';
3
+
4
+ type ComponentType = 'client' | 'server';
5
+
6
+ function renderComponent(name: string, type: ComponentType): string {
7
+ const directive = type === 'client' ? "'use client';\n\n" : '';
8
+
9
+ return `${directive}interface ${name}Props {
10
+ id: string;
11
+ }
12
+
13
+ export default function ${name}({ id }: ${name}Props) {
14
+ return (
15
+ <div>
16
+ <h1>${name}</h1>
17
+ </div>
18
+ );
19
+ }
20
+ `;
21
+ }
22
+
23
+ export function makeComponent(feature: string, name: string, type: ComponentType = 'client'): void {
24
+ const base = ensureFeatureExists(feature, `components/${type}`);
25
+ const filePath = path.join(base, `${name}.tsx`);
26
+
27
+ if (fileExists(filePath)) {
28
+ throw new Error(`Component "${name}" already exists at ${filePath}`);
29
+ }
30
+
31
+ writeFileSafe(filePath, renderComponent(name, type));
32
+ console.log(`✅ Component "${name}" created at ${filePath}`);
33
+ }
@@ -0,0 +1,42 @@
1
+ import * as path from 'path';
2
+ import { ensureFeatureExists, writeFileSafe, fileExists, resolveImport } from '../utils';
3
+
4
+ function renderContainer(containerName: string, name: string, feature: string): string {
5
+ const hookName = `use${name}`;
6
+ const hookPath = resolveImport(feature, `hooks/${hookName}`);
7
+ const componentPath = resolveImport(feature, `components/client/${name}`);
8
+
9
+ return `'use client';
10
+
11
+ import { ${hookName} } from '${hookPath}';
12
+ import ${name} from '${componentPath}';
13
+
14
+ export default function ${containerName}() {
15
+ const { items, loading, error } = ${hookName}();
16
+
17
+ if (loading) return <div>Loading...</div>;
18
+ if (error) return <div>Error: {error}</div>;
19
+
20
+ return (
21
+ <div>
22
+ {items.map((item) => (
23
+ <${name} key={item.id} {...item} />
24
+ ))}
25
+ </div>
26
+ );
27
+ }
28
+ `;
29
+ }
30
+
31
+ export function makeContainer(feature: string, name: string, pascalName?: string): void {
32
+ const base = ensureFeatureExists(feature, 'containers');
33
+ const filePath = path.join(base, `${name}.tsx`);
34
+
35
+ if (fileExists(filePath)) {
36
+ throw new Error(`Container "${name}" already exists at ${filePath}`);
37
+ }
38
+
39
+ const entityName = pascalName ?? name.replace(/Container$/, '');
40
+ writeFileSafe(filePath, renderContainer(name, entityName, feature));
41
+ console.log(`✅ Container "${name}" created at ${filePath}`);
42
+ }
@@ -1,5 +1,12 @@
1
- import * as fs from 'fs';
2
1
  import * as path from 'path';
2
+ import { toPascalCase, featureBasePath, writeFileSafe, mkdirSafe, fileExists, resolveImport } from '../utils';
3
+ import { makeComponent } from './component';
4
+ import { makeHook } from './hook';
5
+ import { makeService } from './service';
6
+ import { makeRepository } from './repository';
7
+ import { makeSchema } from './schema';
8
+ import { makeContainer } from './container';
9
+ import { makeTypes } from './types';
3
10
 
4
11
  const FEATURE_DIRS = [
5
12
  'components/server',
@@ -9,34 +16,59 @@ const FEATURE_DIRS = [
9
16
  'services',
10
17
  'repositories',
11
18
  'schemas',
19
+ 'types',
12
20
  ];
13
21
 
14
- function renderTemplate(name: string): string {
15
- const componentName = name
16
- .split('-')
17
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
18
- .join('');
22
+ function renderPage(name: string, pascalName: string): string {
23
+ const containerPath = resolveImport(name, `containers/${pascalName}Container`, true);
19
24
 
20
- return `export default function ${componentName}Page() {
25
+ return `import ${pascalName}Container from '${containerPath}';
26
+
27
+ export default function ${pascalName}Page() {
21
28
  return (
22
29
  <div>
23
- <h1>${componentName}</h1>
30
+ <${pascalName}Container />
24
31
  </div>
25
32
  );
26
33
  }
27
34
  `;
28
35
  }
29
36
 
30
- export async function makeFeature(name: string) {
31
- const base = path.join(process.cwd(), 'app', name);
37
+ export async function makeFeature(name: string, all: boolean = false): Promise<void> {
38
+ const base = featureBasePath(name);
39
+ const pascalName = toPascalCase(name);
40
+
41
+ if (fileExists(base)) {
42
+ throw new Error(`Feature "${name}" already exists at ${base}`);
43
+ }
32
44
 
33
45
  for (const dir of FEATURE_DIRS) {
34
- fs.mkdirSync(path.join(base, dir), { recursive: true });
35
- fs.writeFileSync(path.join(base, dir, '.gitkeep'), '');
46
+ mkdirSafe(path.join(base, dir));
47
+ if (!all) {
48
+ writeFileSafe(path.join(base, dir, '.gitkeep'), '');
49
+ }
36
50
  }
37
51
 
38
- const page = renderTemplate(name);
39
- fs.writeFileSync(path.join(base, 'page.tsx'), page);
52
+ if (all) {
53
+ writeFileSafe(path.join(base, 'page.tsx'), renderPage(name, pascalName));
54
+ } else {
55
+ writeFileSafe(
56
+ path.join(base, 'page.tsx'),
57
+ `export default function ${pascalName}Page() {\n return (\n <div>\n <h1>${pascalName}</h1>\n </div>\n );\n}\n`
58
+ );
59
+ }
40
60
 
41
61
  console.log(`✅ Feature "${name}" scaffolded at ${base}`);
42
- }
62
+
63
+ if (all) {
64
+ makeTypes(name, pascalName);
65
+ makeSchema(name, pascalName);
66
+ makeRepository(name, pascalName);
67
+ makeService(name, pascalName);
68
+ makeHook(name, `use${pascalName}`, pascalName);
69
+ makeComponent(name, pascalName, 'client');
70
+ makeContainer(name, `${pascalName}Container`, pascalName);
71
+
72
+ console.log(`✅ All files scaffolded for "${name}"`);
73
+ }
74
+ }