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/commands/component.js +10 -11
- package/dist/commands/container.js +22 -15
- package/dist/commands/feature.js +27 -24
- package/dist/commands/hook.js +107 -16
- package/dist/commands/repository.js +32 -18
- package/dist/commands/schema.js +4 -8
- package/dist/commands/service.js +49 -24
- package/dist/commands/types.js +57 -0
- package/dist/index.js +111 -53
- package/dist/utils.js +132 -0
- package/package.json +10 -9
- package/src/__tests__/utils.test.ts +128 -0
- package/src/commands/__tests__/feature.test.ts +85 -0
- package/src/commands/__tests__/imports.test.ts +115 -0
- package/src/commands/__tests__/individual.test.ts +137 -0
- package/src/commands/component.ts +12 -15
- package/src/commands/container.ts +24 -18
- package/src/commands/feature.ts +31 -25
- package/src/commands/hook.ts +109 -19
- package/src/commands/repository.ts +35 -21
- package/src/commands/schema.ts +5 -10
- package/src/commands/service.ts +52 -27
- package/src/commands/types.ts +25 -0
- package/src/index.ts +96 -45
- package/src/utils.ts +115 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +7 -0
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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": "
|
|
15
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
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
|
+
});
|