domain-driver 0.0.5 → 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.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ const commander_1 = require("commander");
4
5
  const feature_1 = require("./commands/feature");
5
6
  const component_1 = require("./commands/component");
6
7
  const hook_1 = require("./commands/hook");
@@ -8,60 +9,117 @@ const service_1 = require("./commands/service");
8
9
  const schema_1 = require("./commands/schema");
9
10
  const repository_1 = require("./commands/repository");
10
11
  const container_1 = require("./commands/container");
11
- const [, , command, feature, name] = process.argv;
12
- if (!command || !feature) {
13
- console.error('Usage: domain-driver <command> [options]');
14
- process.exit(1);
15
- }
16
- switch (command) {
17
- case 'make:feature':
18
- const all = process.argv.includes('-a') || process.argv.includes('-A');
19
- (0, feature_1.makeFeature)(feature, all).then(() => { });
20
- break;
21
- case 'make:component':
22
- if (!name) {
23
- console.error('Usage: domain-driver make:component <feature> <name> [client|server]');
24
- process.exit(1);
25
- }
26
- const type = process.argv[5] || 'client';
27
- (0, component_1.makeComponent)(feature, name, type);
28
- break;
29
- case 'make:hook':
30
- if (!name) {
31
- console.error('Usage: domain-driver make:hook <feature> <name>');
32
- process.exit(1);
33
- }
12
+ const types_1 = require("./commands/types");
13
+ const program = new commander_1.Command();
14
+ program
15
+ .name('domain-driver')
16
+ .description('CLI scaffolding tool for domain-driven development in Next.js')
17
+ .version('0.1.0');
18
+ program
19
+ .command('make:feature <name>')
20
+ .description('Scaffold a full feature folder structure')
21
+ .option('-a, --all', 'Scaffold all files inside each folder')
22
+ .action(async (name, options) => {
23
+ try {
24
+ await (0, feature_1.makeFeature)(name, options.all ?? false);
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : 'Unknown error';
28
+ console.error(`❌ ${message}`);
29
+ process.exit(1);
30
+ }
31
+ });
32
+ program
33
+ .command('make:component <feature> <name>')
34
+ .description('Scaffold a component inside an existing feature')
35
+ .argument('[type]', 'Component type: client or server', 'client')
36
+ .action((feature, name, type) => {
37
+ try {
38
+ const componentType = type === 'server' ? 'server' : 'client';
39
+ (0, component_1.makeComponent)(feature, name, componentType);
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : 'Unknown error';
43
+ console.error(`❌ ${message}`);
44
+ process.exit(1);
45
+ }
46
+ });
47
+ program
48
+ .command('make:container <feature> <name>')
49
+ .description('Scaffold a smart container component inside an existing feature')
50
+ .action((feature, name) => {
51
+ try {
52
+ (0, container_1.makeContainer)(feature, name);
53
+ }
54
+ catch (error) {
55
+ const message = error instanceof Error ? error.message : 'Unknown error';
56
+ console.error(`❌ ${message}`);
57
+ process.exit(1);
58
+ }
59
+ });
60
+ program
61
+ .command('make:hook <feature> <name>')
62
+ .description('Scaffold a custom hook inside an existing feature')
63
+ .action((feature, name) => {
64
+ try {
34
65
  (0, hook_1.makeHook)(feature, name);
35
- break;
36
- case 'make:service':
37
- if (!name) {
38
- console.error('Usage: domain-driver make:service <feature> <name>');
39
- process.exit(1);
40
- }
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : 'Unknown error';
69
+ console.error(`❌ ${message}`);
70
+ process.exit(1);
71
+ }
72
+ });
73
+ program
74
+ .command('make:service <feature> <name>')
75
+ .description('Scaffold single-responsibility service files inside an existing feature')
76
+ .action((feature, name) => {
77
+ try {
41
78
  (0, service_1.makeService)(feature, name);
42
- break;
43
- case 'make:schema':
44
- if (!name) {
45
- console.error('Usage: domain-driver make:schema <feature> <name>');
46
- process.exit(1);
47
- }
48
- (0, schema_1.makeSchema)(feature, name);
49
- break;
50
- case 'make:repository':
51
- if (!name) {
52
- console.error('Usage: domain-driver make:repository <feature> <name>');
53
- process.exit(1);
54
- }
79
+ }
80
+ catch (error) {
81
+ const message = error instanceof Error ? error.message : 'Unknown error';
82
+ console.error(`❌ ${message}`);
83
+ process.exit(1);
84
+ }
85
+ });
86
+ program
87
+ .command('make:repository <feature> <name>')
88
+ .description('Scaffold single-responsibility repository files inside an existing feature')
89
+ .action((feature, name) => {
90
+ try {
55
91
  (0, repository_1.makeRepository)(feature, name);
56
- break;
57
- case 'make:container':
58
- if (!name) {
59
- console.error('Usage: domain-driver make:container <feature> <name>');
60
- process.exit(1);
61
- }
62
- (0, container_1.makeContainer)(feature, name);
63
- break;
64
- default:
65
- console.error(`Unknown command: ${command}`);
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : 'Unknown error';
95
+ console.error(`❌ ${message}`);
96
+ process.exit(1);
97
+ }
98
+ });
99
+ program
100
+ .command('make:schema <feature> <name>')
101
+ .description('Scaffold Zod schemas for create and update operations')
102
+ .action((feature, name) => {
103
+ try {
104
+ (0, schema_1.makeSchema)(feature, name);
105
+ }
106
+ catch (error) {
107
+ const message = error instanceof Error ? error.message : 'Unknown error';
108
+ console.error(`❌ ${message}`);
109
+ process.exit(1);
110
+ }
111
+ });
112
+ program
113
+ .command('make:types <feature> <name>')
114
+ .description('Scaffold a types file inside an existing feature')
115
+ .action((feature, name) => {
116
+ try {
117
+ (0, types_1.makeTypes)(feature, name);
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : 'Unknown error';
121
+ console.error(`❌ ${message}`);
66
122
  process.exit(1);
67
- }
123
+ }
124
+ });
125
+ program.parse();
package/dist/utils.js ADDED
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.toPascalCase = toPascalCase;
37
+ exports.featureBasePath = featureBasePath;
38
+ exports.ensureFeatureExists = ensureFeatureExists;
39
+ exports.writeFileSafe = writeFileSafe;
40
+ exports.mkdirSafe = mkdirSafe;
41
+ exports.fileExists = fileExists;
42
+ exports.detectAlias = detectAlias;
43
+ exports.resetAliasCache = resetAliasCache;
44
+ exports.resolveImport = resolveImport;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ function toPascalCase(name) {
48
+ return name
49
+ .split('-')
50
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
51
+ .join('');
52
+ }
53
+ function featureBasePath(feature) {
54
+ return path.join(process.cwd(), 'app', feature);
55
+ }
56
+ function ensureFeatureExists(feature, subdir) {
57
+ const base = path.join(featureBasePath(feature), subdir);
58
+ if (!fs.existsSync(base)) {
59
+ throw new Error(`Feature "${feature}" does not exist. Run: domain-driver make:feature ${feature}`);
60
+ }
61
+ return base;
62
+ }
63
+ function writeFileSafe(filePath, content) {
64
+ try {
65
+ fs.writeFileSync(filePath, content);
66
+ }
67
+ catch (error) {
68
+ const message = error instanceof Error ? error.message : 'Unknown error';
69
+ throw new Error(`Failed to write ${filePath}: ${message}`);
70
+ }
71
+ }
72
+ function mkdirSafe(dirPath) {
73
+ try {
74
+ fs.mkdirSync(dirPath, { recursive: true });
75
+ }
76
+ catch (error) {
77
+ const message = error instanceof Error ? error.message : 'Unknown error';
78
+ throw new Error(`Failed to create directory ${dirPath}: ${message}`);
79
+ }
80
+ }
81
+ function fileExists(filePath) {
82
+ return fs.existsSync(filePath);
83
+ }
84
+ let cachedAlias;
85
+ function detectAlias() {
86
+ if (cachedAlias)
87
+ return cachedAlias;
88
+ const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
89
+ if (!fs.existsSync(tsconfigPath)) {
90
+ cachedAlias = { alias: null, appDir: 'app' };
91
+ return cachedAlias;
92
+ }
93
+ try {
94
+ const raw = fs.readFileSync(tsconfigPath, 'utf-8');
95
+ const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
96
+ const tsconfig = JSON.parse(stripped);
97
+ const paths = tsconfig?.compilerOptions?.paths;
98
+ if (paths) {
99
+ for (const [key, values] of Object.entries(paths)) {
100
+ const targets = values;
101
+ const hasAppMapping = targets.some((v) => v === './app/*' || v === 'app/*' || v === './src/app/*' || v === 'src/app/*');
102
+ if (hasAppMapping && key.endsWith('/*')) {
103
+ const prefix = key.slice(0, -1);
104
+ const appDir = targets[0].replace('/*', '').replace('./', '');
105
+ cachedAlias = { alias: prefix, appDir };
106
+ return cachedAlias;
107
+ }
108
+ const hasSrcMapping = targets.some((v) => v === './src/*' || v === 'src/*' || v === './*');
109
+ if (hasSrcMapping && key.endsWith('/*')) {
110
+ const prefix = key.slice(0, -1);
111
+ cachedAlias = { alias: prefix, appDir: 'app' };
112
+ return cachedAlias;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ // tsconfig parse failed, fall back to relative imports
119
+ }
120
+ cachedAlias = { alias: null, appDir: 'app' };
121
+ return cachedAlias;
122
+ }
123
+ function resetAliasCache() {
124
+ cachedAlias = undefined;
125
+ }
126
+ function resolveImport(fromFeature, relativePath, fromRoot = false) {
127
+ const { alias } = detectAlias();
128
+ if (alias) {
129
+ return `${alias}app/${fromFeature}/${relativePath}`;
130
+ }
131
+ return fromRoot ? `./${relativePath}` : `../${relativePath}`;
132
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "domain-driver",
3
- "version": "0.0.5",
4
- "description": "",
3
+ "version": "0.1.0",
4
+ "description": "CLI scaffolding tool for domain-driven development in Next.js",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "domain-driver": "./dist/index.js"
@@ -11,17 +11,18 @@
11
11
  "url": "https://github.com/IsaacHatilima/domain-driver"
12
12
  },
13
13
  "scripts": {
14
- "build": "npx tsc",
15
- "dev": "ts-node src/index.ts",
16
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "build": "tsc",
15
+ "test": "vitest run"
17
16
  },
18
17
  "keywords": [],
19
- "author": "",
20
- "license": "ISC",
18
+ "author": "Isaac Hatilima",
19
+ "license": "MIT",
21
20
  "devDependencies": {
22
- "@types/node": "^25.4.0"
21
+ "@types/node": "^25.4.0",
22
+ "typescript": "^5.9.3",
23
+ "vitest": "^4.1.2"
23
24
  },
24
25
  "dependencies": {
25
- "typescript": "^5.9.3"
26
+ "commander": "^14.0.3"
26
27
  }
27
28
  }
@@ -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
+ });